diff --git a/Assets/ContextCircleMenu/Editor/ContextCircleMenuLoader.cs b/Assets/ContextCircleMenu/Editor/ContextCircleMenuLoader.cs index 3d0b7fe..596f98b 100644 --- a/Assets/ContextCircleMenu/Editor/ContextCircleMenuLoader.cs +++ b/Assets/ContextCircleMenu/Editor/ContextCircleMenuLoader.cs @@ -52,7 +52,7 @@ private static void Initialize() { if (_contextCircleMenu != null) RemovePreviousRadialMenu(); _contextCircleMenu = - new ContextCircleMenu(RadialMenuSize.x, RadialMenuSize.y, _activeSceneView.rootVisualElement); + new ContextCircleMenu(RadialMenuSize.x, RadialMenuSize.y, 100f, _activeSceneView.rootVisualElement); if (_onBuild == null) _contextCircleMenu.CreateMenu(builder => @@ -69,7 +69,7 @@ private static void Initialize() _activeSceneView.rootVisualElement.Add(_contextCircleMenu); } - + /// /// Event that allows customization of the Context Circle Menu construction. diff --git a/Assets/ContextCircleMenu/Editor/Core/CircleMenuBuilder.cs b/Assets/ContextCircleMenu/Editor/Core/CircleMenuBuilder.cs index aff38a2..2f28b36 100644 --- a/Assets/ContextCircleMenu/Editor/Core/CircleMenuBuilder.cs +++ b/Assets/ContextCircleMenu/Editor/Core/CircleMenuBuilder.cs @@ -9,15 +9,15 @@ namespace ContextCircleMenu.Editor public sealed class CircleMenuBuilder { private readonly List _factories = new(); + private IButtonFactory _buttonFactory; private IFolderCircleMenuFactory _folderFactory; - private ICircleMenuFactory _rootFactory; internal CircleMenu Build(IMenuControllable menu) { - _rootFactory ??= new RootMenuFactory(); _folderFactory ??= new FolderMenuFactory(); + _buttonFactory ??= new ButtonFactory(); - var root = _rootFactory.Create(); + CircleMenu root = _folderFactory.Create(string.Empty, menu, null, _buttonFactory); foreach (var factory in _factories) { var pathSegments = factory.PathSegments.SkipLast(1); @@ -27,14 +27,17 @@ internal CircleMenu Build(IMenuControllable menu) var child = currentMenu.Children.Find(m => m.Path == pathSegment); if (child == null) { - child = _folderFactory.Create(pathSegment, menu, currentMenu); + child = _folderFactory.Create(pathSegment, menu, currentMenu, _buttonFactory); + var backButton = _buttonFactory.CreateBackButton(menu.Back); + backButton.ShouldCloseMenuAfterSelection = false; + child.PrepareButton(backButton); currentMenu.Children.Add(child); } currentMenu = child; } - currentMenu.Children.Add(factory.Create()); + currentMenu.Children.Add(factory.Create(_buttonFactory)); } return root; @@ -78,5 +81,14 @@ public void ConfigureFolder(IFolderCircleMenuFactory factory) { _folderFactory = factory; } + + /// + /// Sets a custom factory for creating menu buttons, allowing for further customization of menu buttons. + /// + /// The factory to use for creating menu buttons. + public void ConfigureButton(IButtonFactory factory) + { + _buttonFactory = factory; + } } } \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularMenus.meta b/Assets/ContextCircleMenu/Editor/Core/CircleMenus.meta similarity index 100% rename from Assets/ContextCircleMenu/Editor/Core/CircularMenus.meta rename to Assets/ContextCircleMenu/Editor/Core/CircleMenus.meta diff --git a/Assets/ContextCircleMenu/Editor/Core/CircleMenus/CircleMenu.cs b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/CircleMenu.cs new file mode 100644 index 0000000..ef08767 --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/CircleMenu.cs @@ -0,0 +1,109 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UIElements; + +namespace ContextCircleMenu.Editor +{ + /// + /// Represents a menu item in the circle menu. + /// + public abstract class CircleMenu + { + protected internal readonly CircleMenu Parent; + + private bool _alreadyInitialized; + private IButtonFactory _buttonFactory; + + protected CircleButton[] ButtonElements; + protected VisualElement[] UtilityElements; + + protected CircleMenu(string path, GUIContent icon, Action onSelected, CircleMenu parent, IButtonFactory factory, + bool shouldCloseMenuAfterSelection = true) + { + Path = path; + Icon = icon; + OnSelected = onSelected; + Parent = parent; + ShouldCloseMenuAfterSelection = shouldCloseMenuAfterSelection; + _buttonFactory = factory; + } + + public List Children { get; } = new(8); + public GUIContent Icon { get; } + public string Path { get; } + public bool ShouldCloseMenuAfterSelection { get; } + public Action OnSelected { get; protected set; } + + internal ReadOnlySpan BuildElements(ref ContextCircleMenuOption menuOption) + { + if (!_alreadyInitialized) + { + _buttonFactory ??= new ButtonFactory(); + var buttons = CreateButtons(_buttonFactory, ref menuOption); + ButtonElements = ButtonElements == null ? buttons : ButtonElements.Concat(buttons).ToArray(); + UtilityElements = CreateUtilityElements(ref menuOption); + + OnInitialized(ref menuOption); + _alreadyInitialized = true; + } + + OnBuild(); + + var pool = ArrayPool.Shared; + var buffer = pool.Rent(ButtonElements.Length + UtilityElements.Length); + ButtonElements.CopyTo(buffer, 0); + UtilityElements.CopyTo(buffer, ButtonElements.Length); + var combinedSpan = new Span(buffer, 0, ButtonElements.Length + UtilityElements.Length); + pool.Return(buffer); + return combinedSpan; + } + + + internal void PrepareButton(CircleButton button) + { + if (ButtonElements == null) + { + ButtonElements = new[] { button }; + } + else + { + Array.Resize(ref ButtonElements, ButtonElements.Length + 1); + ButtonElements[^1] = button; + } + } + + /// + /// Creates the buttons for the menu. + /// + /// + protected abstract CircleButton[] + CreateButtons(IButtonFactory factory, ref ContextCircleMenuOption menuOption); + + /// + /// Creates the utility elements for the menu. + /// + /// + protected virtual VisualElement[] CreateUtilityElements(ref ContextCircleMenuOption menuOption) + { + return Array.Empty(); + } + + /// + /// Called when the menu is initialized. + /// + /// + protected virtual void OnInitialized(ref ContextCircleMenuOption menuOption) + { + } + + /// + /// Called when the menu is built. + /// + protected virtual void OnBuild() + { + } + } +} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/CircleMenu.cs.meta b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/CircleMenu.cs.meta similarity index 100% rename from Assets/ContextCircleMenu/Editor/Core/CircularMenus/CircleMenu.cs.meta rename to Assets/ContextCircleMenu/Editor/Core/CircleMenus/CircleMenu.cs.meta diff --git a/Assets/ContextCircleMenu/Editor/Core/CircleMenuFactory.cs b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/CircleMenuFactory.cs similarity index 71% rename from Assets/ContextCircleMenu/Editor/Core/CircleMenuFactory.cs rename to Assets/ContextCircleMenu/Editor/Core/CircleMenus/CircleMenuFactory.cs index ba167c0..b363c66 100644 --- a/Assets/ContextCircleMenu/Editor/Core/CircleMenuFactory.cs +++ b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/CircleMenuFactory.cs @@ -24,9 +24,9 @@ public AttributeCircleMenuFactory(ContextCircleMenuAttribute attribute, MethodIn public IEnumerable PathSegments { get; } - public CircleMenu Create() + public CircleMenu Create(IButtonFactory factory) { - return new LeafCircleMenu(PathSegments.Last(), _content, () => _method.Invoke(null, null)); + return new LeafCircleMenu(PathSegments.Last(), _content, () => _method.Invoke(null, null), factory); } } @@ -44,29 +44,17 @@ public CircleMenuFactory(string path, GUIContent content, Action action) public IEnumerable PathSegments { get; } - public CircleMenu Create() + public CircleMenu Create(IButtonFactory factory) { - return new LeafCircleMenu(PathSegments.Last(), _content, _action); - } - } - - public class RootMenuFactory : ICircleMenuFactory - { - public IEnumerable PathSegments => null; - - public CircleMenu Create() - { - return new RootCircleMenu(); + return new LeafCircleMenu(PathSegments.Last(), _content, _action, factory); } } public class FolderMenuFactory : IFolderCircleMenuFactory { - public int Radius { get; set; } = 100; - - public CircleMenu Create(string path, IMenuControllable menu, CircleMenu parent) + public FolderCircleMenu Create(string path, IMenuControllable menu, CircleMenu parent, IButtonFactory factory) { - return new FolderCircleMenu(path, menu.Open, menu.Back, parent, Radius); + return new FolderCircleMenu(path, menu, parent, factory); } } } \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircleMenuFactory.cs.meta b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/CircleMenuFactory.cs.meta similarity index 100% rename from Assets/ContextCircleMenu/Editor/Core/CircleMenuFactory.cs.meta rename to Assets/ContextCircleMenu/Editor/Core/CircleMenus/CircleMenuFactory.cs.meta diff --git a/Assets/ContextCircleMenu/Editor/Core/CircleMenus/FolderCircleMenu.cs b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/FolderCircleMenu.cs new file mode 100644 index 0000000..d8256e5 --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/FolderCircleMenu.cs @@ -0,0 +1,99 @@ +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace ContextCircleMenu.Editor +{ + /// + public class FolderCircleMenu : CircleMenu + { + private Vector3[] _buttonPositions; + + public FolderCircleMenu(string path, IMenuControllable menu, + GUIContent icon, + CircleMenu parent, + IButtonFactory factory) : + base(path, icon, null, parent, factory, false) + { + OnSelected = () => menu.Open(this); + } + + internal FolderCircleMenu(string path, IMenuControllable menu, + CircleMenu parent, + IButtonFactory factory) : + this(path, menu, EditorGUIUtility.IconContent(EditorIcons.FolderIcon), parent, factory) + { + } + + /// + protected override CircleButton[] CreateButtons(IButtonFactory factory, ref ContextCircleMenuOption menuOption) + { + var buttons = new CircleButton[Children.Count]; + for (var index = 0; index < buttons.Length; index++) + { + var item = Children[index]; + var button = factory.Create( + item.Path, + item.Icon, + item.OnSelected, + Children.Count - index); + button.ShouldCloseMenuAfterSelection = item.ShouldCloseMenuAfterSelection; + buttons[index] = button; + } + + return buttons; + } + + /// + protected override VisualElement[] CreateUtilityElements(ref ContextCircleMenuOption menuOption) + { + var label = new Label(Path) + { + style = + { + marginBottom = menuOption.Height * 0.5f + 5.0f, + fontSize = 10, + unityTextAlign = TextAnchor.MiddleCenter, + color = Color.white, + textShadow = new TextShadow + { + offset = new Vector2(0.2f, 0.2f), + blurRadius = 0, + color = Color.black + } + } + }; + return new VisualElement[] { label }; + } + + /// + protected override void OnInitialized(ref ContextCircleMenuOption menuOption) + { + var buttonCount = ButtonElements.Length; + _buttonPositions = new Vector3[buttonCount]; + for (var i = 0; i < buttonCount; i++) + _buttonPositions[i] = GetPositionForIndex(i, buttonCount, menuOption.Radius); + } + + /// + protected override void OnBuild() + { + for (var i = 0; i < ButtonElements.Length; i++) + { + var button = ButtonElements[i]; + button.transform.position = Vector3.zero; + var to = _buttonPositions[i]; + button.experimental.animation.Position(to, 100); + } + } + + private Vector3 GetPositionForIndex(float index, float totalCount, float radius) + { + var angle = index / totalCount * 360f; + return new Vector2( + Mathf.Sin(angle * Mathf.Deg2Rad) * radius, + Mathf.Cos(angle * Mathf.Deg2Rad) * radius + ); + } + } +} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/FolderCircleMenu.cs.meta b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/FolderCircleMenu.cs.meta similarity index 100% rename from Assets/ContextCircleMenu/Editor/Core/CircularMenus/FolderCircleMenu.cs.meta rename to Assets/ContextCircleMenu/Editor/Core/CircleMenus/FolderCircleMenu.cs.meta diff --git a/Assets/ContextCircleMenu/Editor/Core/ICircleMenuFactory.cs b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/ICircleMenuFactory.cs similarity index 57% rename from Assets/ContextCircleMenu/Editor/Core/ICircleMenuFactory.cs rename to Assets/ContextCircleMenu/Editor/Core/CircleMenus/ICircleMenuFactory.cs index c1001c5..81d0b7a 100644 --- a/Assets/ContextCircleMenu/Editor/Core/ICircleMenuFactory.cs +++ b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/ICircleMenuFactory.cs @@ -5,11 +5,11 @@ namespace ContextCircleMenu.Editor public interface ICircleMenuFactory { public IEnumerable PathSegments { get; } - public CircleMenu Create(); + public CircleMenu Create(IButtonFactory factory); } public interface IFolderCircleMenuFactory { - public CircleMenu Create(string path, IMenuControllable menu, CircleMenu parent); + public FolderCircleMenu Create(string path, IMenuControllable menu, CircleMenu parent, IButtonFactory factory); } } \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/ICircleMenuFactory.cs.meta b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/ICircleMenuFactory.cs.meta similarity index 100% rename from Assets/ContextCircleMenu/Editor/Core/ICircleMenuFactory.cs.meta rename to Assets/ContextCircleMenu/Editor/Core/CircleMenus/ICircleMenuFactory.cs.meta diff --git a/Assets/ContextCircleMenu/Editor/Core/CircleMenus/LeafCircleMenu.cs b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/LeafCircleMenu.cs new file mode 100644 index 0000000..4401217 --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/LeafCircleMenu.cs @@ -0,0 +1,20 @@ +using System; +using UnityEngine; + +namespace ContextCircleMenu.Editor +{ + /// + public sealed class LeafCircleMenu : CircleMenu + { + public LeafCircleMenu(string path, GUIContent icon, Action onSelected, IButtonFactory factory, + CircleMenu parent = null) : base(path, icon, onSelected, parent, factory) + { + } + + /// + protected override CircleButton[] CreateButtons(IButtonFactory factory, ref ContextCircleMenuOption menuOption) + { + return null; + } + } +} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/LeafCircleMenu.cs.meta b/Assets/ContextCircleMenu/Editor/Core/CircleMenus/LeafCircleMenu.cs.meta similarity index 100% rename from Assets/ContextCircleMenu/Editor/Core/CircularMenus/LeafCircleMenu.cs.meta rename to Assets/ContextCircleMenu/Editor/Core/CircleMenus/LeafCircleMenu.cs.meta diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularButton.cs b/Assets/ContextCircleMenu/Editor/Core/CircularButton.cs deleted file mode 100644 index b2bc68d..0000000 --- a/Assets/ContextCircleMenu/Editor/Core/CircularButton.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using UnityEditor; -using UnityEngine; -using UnityEngine.UIElements; - -namespace ContextCircleMenu.Editor -{ - public sealed class CircularButton : VisualElement - { - private readonly Button _button; - private readonly Color _hoverColor = new(0.2745098f, 0.3764706f, 0.4862745f, 1.0f); - private readonly Color _normalColor = new(0.02f, 0.02f, 0.02f, 0.8f); - private readonly Action _onSelect; - private readonly bool _shouldCloseMenuAfterSelect; - public bool IsEntered; - - - public CircularButton(string text, GUIContent icon, int section, Action onSelect, - bool shouldCloseMenuAfterSelect = true) - { - _onSelect = onSelect; - _shouldCloseMenuAfterSelect = shouldCloseMenuAfterSelect; - - style.position = Position.Absolute; - style.alignItems = Align.Center; - - _button = new Button(onSelect) - { - style = - { - paddingLeft = 8, - paddingRight = 8, - paddingTop = 4, - paddingBottom = 4, - flexDirection = FlexDirection.Row, - borderTopLeftRadius = 4.0f, - borderBottomLeftRadius = 4.0f, - borderBottomRightRadius = 4.0f, - borderTopRightRadius = 4.0f, - flexGrow = 1, - backgroundColor = _normalColor - }, - text = "" - }; - - var label = new Label - { - style = - { - paddingBottom = 0.0f, - paddingLeft = 0.0f, - paddingRight = 0.0f, - paddingTop = 0.0f, - marginLeft = 5.0f, - marginRight = 5.0f, - flexGrow = 1 - }, - text = text - }; - - if (icon != null) - { - var image = new Image - { - image = icon.image, - style = - { - width = 16.0f, - height = 16.0f, - flexShrink = 0 - } - }; - _button.Add(image); - } - - _button.Add(label); - - if (section != -1) - { - var index = new Label - { - text = section.ToString(), - style = - { - color = new Color(0.7f, 0.7f, 0.7f, 1.0f), - unityFontStyleAndWeight = FontStyle.Italic - } - }; - _button.Add(index); - } - - Add(_button); - - RegisterCallback(OnMouseEnter); - RegisterCallback(OnMouseLeave); - } - - internal bool TryForceSelect() - { - _onSelect?.Invoke(); - return _shouldCloseMenuAfterSelect; - } - - private void OnMouseEnter(MouseEnterEvent evt) - { - IsEntered = true; - _button.style.backgroundColor = _hoverColor; - } - - private void OnMouseLeave(MouseLeaveEvent evt) - { - IsEntered = false; - _button.style.backgroundColor = _normalColor; - } - - - public static CircularButton CreateBackButton(Action back) - { - return new CircularButton("Back", - EditorGUIUtility.IconContent(EditorIcons.Back2x), -1, back, false); - } - } -} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/CircleMenu.cs b/Assets/ContextCircleMenu/Editor/Core/CircularMenus/CircleMenu.cs deleted file mode 100644 index d537b03..0000000 --- a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/CircleMenu.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UIElements; - -namespace ContextCircleMenu.Editor -{ - /// - /// Represents a menu item in the circle menu. - /// - public abstract class CircleMenu - { - private readonly int _radius; - protected internal readonly List Children = new(); - protected internal readonly GUIContent Icon; - 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, - bool shouldCloseMenuAfterSelection = true) - { - Path = path; - Icon = icon; - OnSelected = onSelected; - Parent = parent; - _radius = radius; - ShouldCloseMenuAfterSelection = shouldCloseMenuAfterSelection; - } - - internal ReadOnlySpan CreateElements() - { - _buttonElements ??= CreateButtons(); - _utilityElements ??= CreateUtilityElements(); - - for (var i = 0; i < _buttonElements.Length; i++) - { - var button = _buttonElements[i]; - button.transform.position = Vector3.zero; - var to = Vector2.zero + GetPositionForIndex(i, _buttonElements.Length); - button.experimental.animation.Position(to, 100); - } - - var pool = ArrayPool.Shared; - var buffer = pool.Rent(_buttonElements.Length + _utilityElements.Length); - _buttonElements.CopyTo(buffer, 0); - _utilityElements.CopyTo(buffer, _buttonElements.Length); - var combinedSpan = new Span(buffer, 0, _buttonElements.Length + _utilityElements.Length); - pool.Return(buffer); - return combinedSpan; - } - - /// - /// Creates the buttons for the menu. - /// - /// - protected abstract VisualElement[] CreateButtons(); - - /// - /// Creates the utility elements for the menu. - /// - /// - protected virtual VisualElement[] CreateUtilityElements() - { - return null; - } - - - private Vector2 GetPositionForIndex(float index, float totalCount) - { - var angle = index / totalCount * 360f; - return new Vector2( - Mathf.Sin(angle * Mathf.Deg2Rad) * _radius, - Mathf.Cos(angle * Mathf.Deg2Rad) * _radius - ); - } - } -} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/FolderCircleMenu.cs b/Assets/ContextCircleMenu/Editor/Core/CircularMenus/FolderCircleMenu.cs deleted file mode 100644 index 2afb2ed..0000000 --- a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/FolderCircleMenu.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using UnityEditor; -using UnityEngine; -using UnityEngine.UIElements; - -namespace ContextCircleMenu.Editor -{ - /// - public class FolderCircleMenu : CircleMenu - { - private readonly Action _onBack; - - public FolderCircleMenu(string path, Action onOpen, Action onBack, CircleMenu parent, - int radius = 100) : - base(path, EditorGUIUtility.IconContent(EditorIcons.FolderIcon), - null, parent, radius, false) - { - OnSelected = () => onOpen(this); - _onBack = onBack; - } - - /// - protected override VisualElement[] CreateButtons() - { - var buttons = new VisualElement[Children.Count + 1]; - buttons[0] = CircularButton.CreateBackButton(_onBack); - for (var index = 1; index < buttons.Length; index++) - { - var item = Children[index - 1]; - buttons[index] = - new CircularButton( - item.Children.Count > 0 ? item.Path + "" : item.Path, - item.Icon, - Children.Count - index + 1, - item.OnSelected, - item.ShouldCloseMenuAfterSelection); - } - - return buttons; - } - - /// - protected override VisualElement[] CreateUtilityElements() - { - var label = new Label(Path) - { - style = - { - marginBottom = 100f * 0.5f + 5.0f, - fontSize = 10, - unityTextAlign = TextAnchor.MiddleCenter, - color = Color.white, - textShadow = new TextShadow - { - offset = new Vector2(0.2f, 0.2f), - blurRadius = 0, - color = Color.black - } - } - }; - return new VisualElement[] { label }; - } - } -} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/LeafCircleMenu.cs b/Assets/ContextCircleMenu/Editor/Core/CircularMenus/LeafCircleMenu.cs deleted file mode 100644 index 8dc1c33..0000000 --- a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/LeafCircleMenu.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using UnityEngine; -using UnityEngine.UIElements; - -namespace ContextCircleMenu.Editor -{ - /// - public class LeafCircleMenu : CircleMenu - { - public LeafCircleMenu(string path, GUIContent icon, Action onSelected, CircleMenu parent = null, - int radius = 100) : base(path, icon, onSelected, parent, radius) - { - } - - /// - protected override VisualElement[] CreateButtons() - { - return null; - } - } -} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/RootCircleMenu.cs b/Assets/ContextCircleMenu/Editor/Core/CircularMenus/RootCircleMenu.cs deleted file mode 100644 index ce6bc7e..0000000 --- a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/RootCircleMenu.cs +++ /dev/null @@ -1,37 +0,0 @@ -using UnityEngine.UIElements; - -namespace ContextCircleMenu.Editor -{ - /// - public class RootCircleMenu : CircleMenu - { - public RootCircleMenu() : base("root", default, null, null) - { - } - - /// - protected override VisualElement[] CreateButtons() - { - var buttons = new VisualElement[Children.Count]; - for (var index = 0; index < Children.Count; index++) - { - var item = Children[index]; - buttons[index] = - new CircularButton( - item.Children.Count > 0 ? item.Path + "" : item.Path, - item.Icon, - Children.Count - index, - item.OnSelected, - item.ShouldCloseMenuAfterSelection); - } - - return buttons; - } - - /// - protected override VisualElement[] CreateUtilityElements() - { - return new VisualElement[] { new Label("") }; - } - } -} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/RootCircleMenu.cs.meta b/Assets/ContextCircleMenu/Editor/Core/CircularMenus/RootCircleMenu.cs.meta deleted file mode 100644 index 0ae4429..0000000 --- a/Assets/ContextCircleMenu/Editor/Core/CircularMenus/RootCircleMenu.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 624f0d0075734e05aca6da80ad0555c8 -timeCreated: 1713976308 \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/ClircleButtons.meta b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons.meta new file mode 100644 index 0000000..b29e930 --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a1bc21fb923548c2b66d294f4c889c63 +timeCreated: 1715644602 \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/ButtonFactory.cs b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/ButtonFactory.cs new file mode 100644 index 0000000..44b3b61 --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/ButtonFactory.cs @@ -0,0 +1,19 @@ +using System; +using UnityEditor; +using UnityEngine; + +namespace ContextCircleMenu.Editor +{ + public class ButtonFactory : IButtonFactory + { + public CircleButton Create(string path, GUIContent icon, Action onSelected, int section) + { + return new SimpleCircleButton(path, icon, section, onSelected); + } + + public CircleButton CreateBackButton(Action onBack) + { + return new SimpleCircleButton("Back", EditorGUIUtility.IconContent(EditorIcons.Back2x), -1, onBack); + } + } +} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/ButtonFactory.cs.meta b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/ButtonFactory.cs.meta new file mode 100644 index 0000000..35a9782 --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/ButtonFactory.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 925ee182e9d94ae792d656e54ed4231f +timeCreated: 1715646161 \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/CircleButton.cs b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/CircleButton.cs new file mode 100644 index 0000000..c864533 --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/CircleButton.cs @@ -0,0 +1,66 @@ +using System; +using UnityEngine; +using UnityEngine.UIElements; + +namespace ContextCircleMenu.Editor +{ + public abstract class CircleButton : VisualElement + { + private readonly Action _onSelect; + private Button _button; + + protected CircleButton(string text, GUIContent icon, int section, Action onSelect) + { + _onSelect = onSelect; + + Section = section; + + Initialize(text, icon, section); + } + + internal bool ShouldCloseMenuAfterSelection { get; set; } = true; + + public bool IsEntered { get; private set; } + public int Section { get; private set; } + private void Initialize(string text, GUIContent icon, int section) + { + style.position = Position.Absolute; + style.alignItems = Align.Center; + + _button = new Button(_onSelect); + ModifierButton(_button, text, icon, section); + Add(_button); + + RegisterCallback(InternalOnMouseEnter); + RegisterCallback(InternalOnMouseLeave); + } + + protected abstract void ModifierButton(Button button, string text, GUIContent icon, int section); + + internal bool TryForceSelect() + { + _onSelect?.Invoke(); + return ShouldCloseMenuAfterSelection; + } + + private void InternalOnMouseEnter(MouseEnterEvent evt) + { + IsEntered = true; + OnMouseEnter(_button, evt); + } + + protected virtual void OnMouseEnter(Button button, MouseEnterEvent evt) + { + } + + private void InternalOnMouseLeave(MouseLeaveEvent evt) + { + IsEntered = false; + OnMouseLeave(_button, evt); + } + + protected virtual void OnMouseLeave(Button button, MouseLeaveEvent evt) + { + } + } +} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/CircularButton.cs.meta b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/CircleButton.cs.meta similarity index 100% rename from Assets/ContextCircleMenu/Editor/Core/CircularButton.cs.meta rename to Assets/ContextCircleMenu/Editor/Core/ClircleButtons/CircleButton.cs.meta diff --git a/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/IButtonFactory.cs b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/IButtonFactory.cs new file mode 100644 index 0000000..46a996b --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/IButtonFactory.cs @@ -0,0 +1,12 @@ +using System; +using UnityEngine; + +namespace ContextCircleMenu.Editor +{ + public interface IButtonFactory + { + public CircleButton Create(string path, GUIContent icon, Action onSelected, int section); + + public CircleButton CreateBackButton(Action onBack); + } +} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/IButtonFactory.cs.meta b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/IButtonFactory.cs.meta new file mode 100644 index 0000000..99db266 --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/IButtonFactory.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bd92d51777ad4b77957c94541043b5f0 +timeCreated: 1715649254 \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/SimpleCircleButton.cs b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/SimpleCircleButton.cs new file mode 100644 index 0000000..03a0a83 --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/SimpleCircleButton.cs @@ -0,0 +1,89 @@ +using System; +using UnityEngine; +using UnityEngine.UIElements; + +namespace ContextCircleMenu.Editor +{ + public class SimpleCircleButton : CircleButton + { + private readonly Color _hoverColor = new(0.2745098f, 0.3764706f, 0.4862745f, 1.0f); + private readonly Color _normalColor = new(0.02f, 0.02f, 0.02f, 0.8f); + + public SimpleCircleButton(string text, GUIContent icon, int section, Action onSelect) + : base(text, icon, section, onSelect) + { + } + + protected override void ModifierButton(Button button, string text, GUIContent icon, int section) + { + button.style.paddingLeft = 8f; + button.style.paddingRight = 8f; + button.style.paddingTop = 4f; + button.style.paddingBottom = 4f; + button.style.flexDirection = FlexDirection.Row; + button.style.borderTopLeftRadius = 4f; + button.style.borderBottomLeftRadius = 4f; + button.style.borderBottomRightRadius = 4f; + button.style.borderTopRightRadius = 4f; + button.style.flexGrow = 1; + button.style.backgroundColor = _normalColor; + button.text = ""; + + var label = new Label + { + style = + { + paddingBottom = 0f, + paddingLeft = 0f, + paddingRight = 0f, + paddingTop = 0f, + marginLeft = 5f, + marginRight = 5f, + flexGrow = 1 + }, + text = text + }; + + if (icon != null) + { + var image = new Image + { + image = icon.image, + style = + { + width = 16f, + height = 16f, + flexShrink = 0 + } + }; + button.Add(image); + } + + button.Add(label); + + if (section != -1) + { + var index = new Label + { + text = section.ToString(), + style = + { + color = new Color(0.7f, 0.7f, 0.7f, 1.0f), + unityFontStyleAndWeight = FontStyle.Italic + } + }; + button.Add(index); + } + } + + protected override void OnMouseEnter(Button button, MouseEnterEvent evt) + { + button.style.backgroundColor = _hoverColor; + } + + protected override void OnMouseLeave(Button button, MouseLeaveEvent evt) + { + button.style.backgroundColor = _normalColor; + } + } +} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/SimpleCircleButton.cs.meta b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/SimpleCircleButton.cs.meta new file mode 100644 index 0000000..bafc407 --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/ClircleButtons/SimpleCircleButton.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 59172d3eb1194cdd992880de8aa1bdb2 +timeCreated: 1715644710 \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/ContextCircleMenu.cs b/Assets/ContextCircleMenu/Editor/Core/ContextCircleMenu.cs index 3040191..9952bfd 100644 --- a/Assets/ContextCircleMenu/Editor/Core/ContextCircleMenu.cs +++ b/Assets/ContextCircleMenu/Editor/Core/ContextCircleMenu.cs @@ -14,12 +14,11 @@ public class ContextCircleMenu : VisualElement, IMenuControllable private static readonly Color AnnulusColor = new(0.02f, 0.02f, 0.02f, 0.8f); private static readonly Color MouseAngleIndicatorBackgroundColor = new(0.01f, 0.01f, 0.01f, 1.0f); private static readonly Color MouseAngleIndicatorForegroundColor = Color.white; - private readonly float _height; private readonly VisualElement _target; - private readonly float _width; private float _currentMouseAngle; private Vector2 _mousePosition; + private ContextCircleMenuOption _option; private Vector2 _position; private CircleMenu _selectedMenu; @@ -29,11 +28,11 @@ public class ContextCircleMenu : VisualElement, IMenuControllable /// /// Width of the menu. /// Height of the menu. + /// Radius of the menu. /// The UI element in which the menu will appear. - public ContextCircleMenu(float width, float height, VisualElement target) + public ContextCircleMenu(float width, float height, float radius, VisualElement target) { - _width = width; - _height = height; + _option = new ContextCircleMenuOption(radius, height, width); _target = target; style.position = Position.Absolute; @@ -73,7 +72,7 @@ public void Show() style.display = DisplayStyle.Flex; _position = _mousePosition; - transform.position = _position - new Vector2(_width * 0.5f, _height * 0.5f); + transform.position = _position - new Vector2(_option.Width * 0.5f, _option.Height * 0.5f); Rebuild(); } @@ -94,7 +93,6 @@ public void Hide() public void Open(CircleMenu menu) { if (!IsVisible) return; - _selectedMenu = menu; Rebuild(); } @@ -107,19 +105,18 @@ public void Back() if (_selectedMenu.Parent != null) Open(_selectedMenu.Parent); } - private void OnAttach(AttachToPanelEvent evt) { generateVisualContent += OnGenerateVisualContent; - _target.RegisterCallback(UpdateMousePosition); - _target.RegisterCallback(OnClick); + _target.RegisterCallback(UpdateMousePosition, TrickleDown.TrickleDown); + _target.RegisterCallback(OnClick, TrickleDown.TrickleDown); } private void OnDetach(DetachFromPanelEvent evt) { generateVisualContent -= OnGenerateVisualContent; - _target.UnregisterCallback(UpdateMousePosition); - _target.UnregisterCallback(OnClick); + _target.UnregisterCallback(UpdateMousePosition, TrickleDown.TrickleDown); + _target.UnregisterCallback(OnClick, TrickleDown.TrickleDown); } private void UpdateMousePosition(MouseMoveEvent evt) @@ -147,21 +144,47 @@ private void OnClick(ClickEvent evt) /// public bool TryForceSelect() { - var button = Children().OfType().FirstOrDefault(b => b.IsEntered); + var button = Children().OfType().FirstOrDefault(b => b.IsEntered); return button != null && button.TryForceSelect(); } + public bool TryForceEnterByMousePosition() + { + var anglePerRegion = 360f / (Children().Count() - 1); + + var currentAngle = _currentMouseAngle % 360; + if (currentAngle < 0) currentAngle += 360; + + var region = (int)(currentAngle / anglePerRegion); + if (region <= 0) region = Children().Count() - 1; + + var otherButtons = Children().OfType().Where(x => x.Section != region); + foreach (var otherButton in otherButtons) + { + var mouseLeaveEvent = MouseLeaveEvent.GetPooled(); + mouseLeaveEvent.target = otherButton; + otherButton.SendEvent(mouseLeaveEvent); + } + + var button = Children().OfType().FirstOrDefault(x => x.Section == region); + if (button == null) return true; + var mouseEnterEvent = MouseEnterEvent.GetPooled(); + mouseEnterEvent.target = button; + button.SendEvent(mouseEnterEvent); + return false; + } + private void Rebuild() { Clear(); - var elements = _selectedMenu.CreateElements(); + var elements = _selectedMenu.BuildElements(ref _option); for (var i = 0; i < elements.Length; i++) Add(elements[i]); } private void OnGenerateVisualContent(MeshGenerationContext context) { - var position = new Vector2(_width * 0.5f, _height * 0.5f); - var radius = _width * 0.1f; + var position = new Vector2(_option.Width * 0.5f, _option.Height * 0.5f); + var radius = _option.Width * 0.1f; var startAngle = _currentMouseAngle + 90.0f - IndicatorSizeDegrees * 0.5f; var endAngle = _currentMouseAngle + 90.0f + IndicatorSizeDegrees * 0.5f; diff --git a/Assets/ContextCircleMenu/Editor/Core/ContextCircleMenuOption.cs b/Assets/ContextCircleMenu/Editor/Core/ContextCircleMenuOption.cs new file mode 100644 index 0000000..7f53b4a --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/ContextCircleMenuOption.cs @@ -0,0 +1,16 @@ +namespace ContextCircleMenu.Editor +{ + public struct ContextCircleMenuOption + { + public ContextCircleMenuOption(float radius, float height, float width) + { + Radius = radius; + Height = height; + Width = width; + } + + public readonly float Radius; + public readonly float Height; + public readonly float Width; + } +} \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Editor/Core/ContextCircleMenuOption.cs.meta b/Assets/ContextCircleMenu/Editor/Core/ContextCircleMenuOption.cs.meta new file mode 100644 index 0000000..6179b9f --- /dev/null +++ b/Assets/ContextCircleMenu/Editor/Core/ContextCircleMenuOption.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 79090acad5c84c3abc55b874b127d8ce +timeCreated: 1715833322 \ No newline at end of file diff --git a/Assets/ContextCircleMenu/Samples~/Custom/Editor/CustomMenu.cs b/Assets/ContextCircleMenu/Samples~/Custom/Editor/CustomMenu.cs index 28c2779..eb38ca4 100644 --- a/Assets/ContextCircleMenu/Samples~/Custom/Editor/CustomMenu.cs +++ b/Assets/ContextCircleMenu/Samples~/Custom/Editor/CustomMenu.cs @@ -1,6 +1,8 @@ -using ContextCircleMenu.Editor; +using System; +using ContextCircleMenu.Editor; using UnityEditor; using UnityEngine; +using UnityEngine.UIElements; namespace ContextCircleMenu.Custom { @@ -12,8 +14,127 @@ public static void Initialize() ContextCircleMenuLoader.OnBuild += builder => { builder.AddMenu("Custom/Debug Test", new GUIContent(), () => Debug.Log("custom/test")); + builder.AddMenu("Custom/Debug Test 2", EditorGUIUtility.IconContent(EditorIcons.ConsoleInfoIcon2x), + () => Debug.Log("custom/test2")); + builder.AddMenu("Custom/Debug Test 3", EditorGUIUtility.IconContent(EditorIcons.ConsoleInfoIcon2x), + () => Debug.Log("custom/test3")); + + for (var i = 0; i < 5; i++) + { + var i1 = i; + builder.AddMenu($"Custom/a/Debug Test {i}", + EditorGUIUtility.IconContent(EditorIcons.ConsoleInfoIcon2x), + () => Debug.Log($"custom/test{i1}")); + } + + for (var i = 0; i < 6; i++) + { + var i1 = i; + builder.AddMenu($"Custom/b/Debug Test {i}", + EditorGUIUtility.IconContent(EditorIcons.ConsoleInfoIcon2x), + () => Debug.Log($"custom/test{i1}")); + } + builder.AddMenu("Debug Test", new GUIContent(), () => Debug.Log("test")); + builder.AddMenu("Debug Test 2", EditorGUIUtility.IconContent(EditorIcons.ConsoleInfoIcon2x), + () => Debug.Log("test2")); + builder.ConfigureButton(new CustomButtonFactory()); + builder.ConfigureFolder(new CustomFolderMenuFactory()); + }; + } + } + + public class CustomFolderMenuFactory : IFolderCircleMenuFactory + { + public FolderCircleMenu Create(string path, IMenuControllable menu, CircleMenu parent, IButtonFactory factory) + { + return new CustomFolderCircleMenu(path, menu, parent, factory); + } + } + + public class CustomFolderCircleMenu : FolderCircleMenu + { + public CustomFolderCircleMenu(string path, IMenuControllable menu, CircleMenu parent, IButtonFactory factory) : + base(path, menu, EditorGUIUtility.IconContent(EditorIcons.FolderIcon), parent, factory) + { + } + + protected override VisualElement[] CreateUtilityElements(ref ContextCircleMenuOption menuOption) + { + var element = new VisualElement(); + var option = menuOption; + element.generateVisualContent += context => + { + var painter = context.painter2D; + var buttonCount = ButtonElements.Length; + for (var i = 0; i < buttonCount; i++) + { + var angle = (float)i / buttonCount * 360f; + if (buttonCount % 2 == 1) + angle += 180f; + else + angle += 180f - 360f / buttonCount / 2; + var vector = new Vector2( + Mathf.Sin(Mathf.Deg2Rad * angle), + Mathf.Cos(Mathf.Deg2Rad * angle)).normalized; + + var from = vector * 12f; + var to = vector * option.Radius * 1.5f; + painter.strokeColor = Color.black; + painter.lineWidth = 2f; + painter.BeginPath(); + painter.MoveTo(from); + painter.LineTo(to); + painter.Stroke(); + } + + painter.BeginPath(); + painter.Arc(Vector2.zero, option.Radius * 1.5f, 0, 360f); + painter.fillColor = new Color(0f, 0f, 0f, 0.2f); + painter.Fill(); + + painter.DrawCircle(Vector2.zero, option.Radius * 1.5f, 0, 360f, 5f, Color.gray); + }; + return new[] { element }; + } + } + + public class CustomButtonFactory : IButtonFactory + { + public CircleButton Create(string path, GUIContent icon, Action onSelected, int section) + { + return new OnlyImageCircleButton(path, icon, section, onSelected); + } + + public CircleButton CreateBackButton(Action onBack) + { + return new OnlyImageCircleButton("Back", EditorGUIUtility.IconContent(EditorIcons.Back2x), + -1, onBack); + } + } + + public class OnlyImageCircleButton : CircleButton + { + public OnlyImageCircleButton(string text, GUIContent icon, int section, Action onSelect) : base(text, icon, + section, onSelect) + { + } + + protected override void ModifierButton(Button button, string text, GUIContent icon, int section) + { + var image = new Image + { + image = icon.image, + style = + { + width = 32f, + height = 32f, + flexShrink = 0 + }, + tooltip = text }; + + button.Add(image); } } } \ No newline at end of file diff --git a/Assets/ContextCircleMenu/package.json b/Assets/ContextCircleMenu/package.json index 539e70f..75ee5f1 100644 --- a/Assets/ContextCircleMenu/package.json +++ b/Assets/ContextCircleMenu/package.json @@ -1,7 +1,7 @@ { "name": "com.garume.context-circle-menu", "displayName": "Context Circle Menu", - "version": "0.3.0", + "version": "1.0.0", "unity": "2022.3", "license": "MIT", "category": "UI", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 1c2845a..6cdad5f 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -27,7 +27,7 @@ "source": "builtin", "dependencies": { "com.unity.ide.visualstudio": "2.0.22", - "com.unity.ide.rider": "3.0.27", + "com.unity.ide.rider": "3.0.28", "com.unity.ide.vscode": "1.2.5", "com.unity.editorcoroutines": "1.0.0", "com.unity.performance.profile-analyzer": "1.2.2", @@ -36,7 +36,7 @@ } }, "com.unity.ide.rider": { - "version": "3.0.27", + "version": "3.0.28", "depth": 1, "source": "registry", "dependencies": { diff --git a/README.md b/README.md index 24590a2..2affaa4 100644 --- a/README.md +++ b/README.md @@ -20,40 +20,31 @@ Context Circle Menu is a simple open-source tool for Unity. It lets users open a ### Feature - Create Context Circle Menu - -![alt text](docs/image.png) - - Easy to use Editor Icon - - Customized Menu - - Add from Attribute - - ![alt text](docs/image-1.png) - - Add Manual - - ![alt text](docs/image-2.png) - + - Button design + - Folder design - Open in Scene View - Customized Shortcut Key -![alt text](docs/image-3.png) - ## Table of Contents - [Context Circle Menu](#context-circle-menu) - - [Overview](#overview) - - [Feature](#feature) - - [Table of Contents](#table-of-contents) - - [Setup](#setup) - - [Requirements](#requirements) - - [Installation](#installation) - - [Demonstration](#demonstration) - - [Editor Icons](#editor-icons) - - [Customized Menu](#customized-menu) - - [Manual Add Method](#manual-add-method) - - [Customized Button](#customized-button) - - [Customized Shortcut Key](#customized-shortcut-key) + - [Overview](#Overview) + - [Features](#Features) + - [Table of Contents](#Table-of-Contents) + - [Setup](#Setup) + - [Requirements](#Requirements) + - [Installation](#Installation) + - [Demonstration](#Demonstration) + - [Editor Icons](#editor-icons) + - [Add Manual Method](#Add-Manual-Method) + - [Customize](#Customize) + - [Customize Buttons](#Customize-Buttons) + - [Customize Folder](#Customize-Folder) + - [Customize Shortcut](#Customized-Shortcut-Key) + - [API Documentation](#API-Documentation) - [LISENCE](#lisence) - [AUTHOR](#author) @@ -121,7 +112,7 @@ Then you will see below menu. [![Image from Gyazo](https://i.gyazo.com/39b665e8fdd473bb408102e1b5d5bf09.gif)](https://gyazo.com/39b665e8fdd473bb408102e1b5d5bf09) -## Editor Icons +### Editor Icons Icons can be attached to menu buttons. @@ -136,9 +127,6 @@ public static void TestMethod() Debug.Log("TestMethod"); } ``` - -## Customized Menu - ### Manual Add Method If you do not want to use the `Context Circle Menu` attribute, you can register the method manually. @@ -161,25 +149,94 @@ public class Menu } ``` +## Customization + ### Customized Button -If you don't like the button UI, you can replace it with your own. +If you don't like the UI of the button, you can replace it with your own button. -Use `builder.ConfigureFolder();` +Use `builder.ConfigureButton` in `ContextCircleMenuLoader.OnBuild`. -> [!CAUTION] -> It is an incomplete feature. -> -> Destructive changes may be made. +```cs +ContextCircleMenuLoader.OnBuild += (builder => +{ + ... + builder.ConfigureButton(FolderMenuFactory); +}); +``` + +To create your own button, you must create a class that extends CircleButton and a corresponding Factory class. -First, you need to create a FolderMenu that extends `CircleMenu`. -You can create buttons freely in `CreateButtons`. -Please refer to `FolderCircleMenu.cs` for detailed code. +Here is an example code similar to the one provided in Sample's Custom. -Next, create a FolderMenuFactory that implements `IFolderCircleMenuFactory`. -Please refer to `CircleMenuFactory` for detailed code. +Samples can be imported from Package Manager > ContextCircleMenu > Samples. -Finally, you can replace the UI by doing the below. +In this example, the button is replaced with a button that displays only an icon. + +Create the following class + +```cs +public class CustomButtonFactory : IButtonFactory +{ + public CircleButton Create(string path, GUIContent icon, Action onSelected, int section) + { + return new OnlyImageCircleButton(path, icon, section, onSelected); + } + + // Back button is needed when creating a folder structure menu. + // section should be -1 unless ConfigureFolder is used. + public CircleButton CreateBackButton(Action onBack) + { + return new OnlyImageCircleButton("Back", EditorGUIUtility.IconContent(EditorIcons.Back2x), + -1, onBack); + } +} + +public class OnlyImageCircleButton : CircleButton +{ + public OnlyImageCircleButton(string text, GUIContent icon, int section, Action onSelect) : base(text, icon, section, onSelect) + { + } + + // You can edit the generated buttons. + // Feel free to customize the buttons here. + protected override void ModifierButton(Button button, string text, GUIContent icon, int section) + { + var image = new Image + { + image = icon.image, + style = + { + width = 32f, + height = 32f, + flexShrink = 0 + }, + tooltip = text + }; + + button.Add(image); + } +} + +``` + +Set the created Factory class. + +```cs +ContextCircleMenuLoader.OnBuild += (builder => +{ + ... + builder.ConfigureButton(new CustomButtonFactory()); +}); +``` + +It will then be replaced by a button that displays only the icon as shown below. + +![alt text](docs/image-6.png) + +### Customize Folder + +If you don't like the folder UI, you can replace it with your own folder. ```cs ContextCircleMenuLoader.OnBuild += (builder => @@ -189,7 +246,93 @@ ContextCircleMenuLoader.OnBuild += (builder => }); ``` -## Customized Shortcut Key +> [!CAUTION] +> Destructive changes were made in v1.0.0. + +To create your own folder, you need to create a class that extends FolderCircleMenu and a corresponding Factory class. + + +Here is an example code similar to the one provided in Sample's Custom. + +Samples can be imported from Package Manager > ContextCircleMenu > Samples. + +In this example, we are replacing an existing UI with vector graphics. + +Create the following classes + +```cs +public class CustomFolderMenuFactory : IFolderCircleMenuFactory +{ + public FolderCircleMenu Create(string path, IMenuControllable menu, CircleMenu parent, IButtonFactory factory) + { + return new CustomFolderCircleMenu(path, menu, parent, factory); + } +} + +public class CustomFolderCircleMenu : FolderCircleMenu +{ + public CustomFolderCircleMenu(string path, IMenuControllable menu, CircleMenu parent, IButtonFactory factory) : + base(path, menu, EditorGUIUtility.IconContent(EditorIcons.FolderIcon), parent, factory) + { + } + + protected override VisualElement[] CreateUtilityElements(ref ContextCircleMenuOption menuOption) + { + var element = new VisualElement(); + var option = menuOption; + element.generateVisualContent += context => + { + var painter = context.painter2D; + var buttonCount = ButtonElements.Length; + for (var i = 0; i < buttonCount; i++) + { + var angle = (float)i / buttonCount * 360f; + if (buttonCount % 2 == 1) + angle += 180f; + else + angle += 180f - 360f / buttonCount / 2; + var vector = new Vector2( + Mathf.Sin(Mathf.Deg2Rad * angle), + Mathf.Cos(Mathf.Deg2Rad * angle)).normalized; + + var from = vector * 12f; + var to = vector * option.Radius * 1.5f; + painter.strokeColor = Color.black; + painter.lineWidth = 2f; + painter.BeginPath(); + painter.MoveTo(from); + painter.LineTo(to); + painter.Stroke(); + } + + painter.BeginPath(); + painter.Arc(Vector2.zero, option.Radius * 1.5f, 0, 360f); + painter.fillColor = new Color(0f, 0f, 0f, 0.2f); + painter.Fill(); + + painter.DrawCircle(Vector2.zero, option.Radius * 1.5f, 0, 360f, 5f, Color.gray); + }; + return new[] { element }; + } +} +``` + +Set the created Factory class. + +```cs +ContextCircleMenuLoader.OnBuild += (builder => +{ + ... + builder.ConfigureButton(new CustomButtonFactory()); + builder.ConfigureFolder(new CustomFolderMenuFactory()); +}); +``` + +Then it will be replaced by the following UI. + +! [alt text](docs/image-5.png) + +### Customized Shortcut Key The default menu open/close button is set to the `A` key, but can be changed freely. @@ -201,6 +344,63 @@ Search for `Context Circle Menu` and you will find the settings as shown in the Set the keys as you like. +## API Documentation + +This section describes the major APIs and can be used as a reference when customizing the UI. + +### ContextCircleMenu + +#### property +| property name | description | +| ---- | ---- | +| BlockMouseEvents | Disables mouse operations such as clicking if true. | + +#### method +| method name | description | +| ---- | ---- | +| Show() | Show menu. | +| Hide() | Hide menu. | +| Open(CircleMenu menu) | Opens the menu specified in the argument. | +| Back() | Open the previous menu. | +| TryForceSelect() | If there is a button in focus, it is forced to select it. | +| TryForceEnterByMousePosition() | Forces the button corresponding to the mouse position to focus. | +| CreateMenu(Action\ configureMenu) | Create the menu content using CircleMenuBuilder. | + +### CircleMenuBuilder +#### method +| method name | description | +| ---- | ---- | +| AddMenu(ICircleMenuFactory factory) | Add custom menu. | +| AddMenu(string path, GUIContent content, Action action) | Add a menu manually. | +| AddMenu(ContextCircleMenuAttribute attribute, MethodInfo method) | Add a menu from the attributes. | +| ConfigureFolder(IFolderCircleMenuFactory factory) | Replace with your custom folder menu. | +| ConfigureButton(IButtonFactory factory) | Replace with your custom button. | + +### CircleMenu +#### abstract method +| method name | description | +| ---- | ---- | +| CreateButtons(IButtonFactory factory, ref ContextCircleMenuOption menuOption) | Create a button to be displayed on the menu. The IButtonFactory passed here will be the one set in CircleMenuBuilder.ConfigureButton(). | + +#### virtual method +| method name | description | +| ---- | ---- | +| CreateUtilityElements(ref ContextCircleMenuOption menuOption) | Create elements other than buttons. | +| OnInitialized(ref ContextCircleMenuOption menuOption) | Called at initialization. | +| OnBuild() | Called when an element is created. Mainly when Show() or Open() is called. | + +### CircleButton +#### abstract method +| method name | description | +| ---- | ---- | +| ModifierButton(Button button, string text, GUIContent icon, int section) | Called when creating a button. Use it to modify the elements of the button. | + +#### virtual method +| method name | description | +| ---- | ---- | +| OnMouseEnter(Button button, MouseEnterEvent evt) | Called when the mouse enters an element.| +| OnMouseLeave(Button button, MouseLeaveEvent evt) | Called when the mouse leaves an element. | + ## LISENCE MIT diff --git a/README_JA.md b/README_JA.md index 11f3ea2..e1db9dc 100644 --- a/README_JA.md +++ b/README_JA.md @@ -9,7 +9,7 @@ Context Circle Menu は、Sceneビュー上で円形のメニューを開き、 ## 概要 -Context Circle Menu はUnity用のシンプルなツールです。円形のメニューを開くことができるVisualElementを提供します。また、Sceneビュー上でメニューを開くローダー機能を搭載しています。このメニューにより、任意のメソッドを素早く使用することができます。円形にボタンが広がるため少ないマウス操作と直観的な理解で開発効率を極限まで高めることが可能です。 +Context Circle Menu はUnity用のシンプルなツールです。円形のメニューを開くことができるVisualElementを提供します。また、Sceneビュー上でメニューを開くローダー機能を搭載しています。このメニューは円形にボタンが広がるため少ないマウス操作と直観的な理解で任意のメソッドを実行させることができます。開発効率を極限まで高まることが期待できます。 [![Image from Gyazo](https://i.gyazo.com/8124142a3643fb0d735f7dd66b068142.gif)](https://gyazo.com/8124142a3643fb0d735f7dd66b068142) @@ -18,26 +18,15 @@ Context Circle Menu はUnity用のシンプルなツールです。円形のメ ### 特徴 - 円形メニューの作成 - -![alt text](docs/image.png) - - 使いやすいエディターアイコン - - メニューのカスタマイズ - - アトリビュートからメソッドを追加 - - ![alt text](docs/image-1.png) - - 手動でメソッドを追加 - - ![alt text](docs/image-2.png) - + - ボタンのデザイン + - フォルダーのデザイン - Sceneビュー上でメニューを開く - ショートカットキーのカスタマイズ -![alt text](docs/image-3.png) - ## 目次 - [Context Circle Menu](#context-circle-menu) - [概要](#概要) @@ -47,11 +36,13 @@ Context Circle Menu はUnity用のシンプルなツールです。円形のメ - [要求](#要求) - [インストール](#インストール) - [デモ](#デモ) - - [Editor Icons](#editor-icons) - - [メニューのカスタマイズ](#メニューのカスタマイズ) + - [Editor Icons](#editor-icons) - [手動メソッド追加](#手動メソッド追加) + - [カスタマイズ](#カスタマイズ) - [ボタンのカスタマイズ](#ボタンのカスタマイズ) - - [ショートカットキーのカスタマイズ](#ショートカットキーのカスタマイズ) + - [フォルダのカスタマイズ](#フォルダのカスタマイズ) + - [ショートカットキーのカスタマイズ](#ショートカットキーのカスタマイズ) + - [API ドキュメント](#API-ドキュメント) - [LISENCE](#lisence) - [AUTHOR](#author) @@ -83,7 +74,7 @@ https://github.com/Garume/ContextCircleMenu.git?path=/Assets/ContextCircleMenu ## デモ -追加したい静的メソッドに `Context Circle Menu` 属性を適用するだけで追加できます。 +静的メソッドに `ContextCircleMenu` 属性を適用するだけで追加できます。 ```cs public class Menu @@ -96,9 +87,9 @@ public class Menu } ``` -すると下のようにメニューが作成されます。 +下図のようにメニューが作成されます。 -Sceneビュー上で`A`キーを押すことでメニューを開けます。 +メニューはSceneビュー上で`A`キーを押すことで開くことができます。 [![Image from Gyazo](https://i.gyazo.com/1ec027f73700f52c6b3cd9691647a8a1.gif)](https://gyazo.com/1ec027f73700f52c6b3cd9691647a8a1) @@ -115,11 +106,11 @@ public class Menu } ``` -すると下のようにメニューが作成されます。 +すると下図のようにフォルダ階層付きのメニューが作成されます。 [![Image from Gyazo](https://i.gyazo.com/39b665e8fdd473bb408102e1b5d5bf09.gif)](https://gyazo.com/39b665e8fdd473bb408102e1b5d5bf09) -## Editor Icons +### Editor Icons メニューボタンにアイコンを付けることができます。 @@ -135,13 +126,11 @@ public static void TestMethod() } ``` -## メニューのカスタマイズ - ### 手動メソッド追加 -もし `Context Circle Menu` 属性を使いたくない場合は、手動でメソッドを登録することができます。 +`Context Circle Menu` 属性を使用する以外に、手動で追加することもできます。 -内部的にメソッドを登録するプロセスを`ContextCircleMenuLoader.OnBuild`でフックすることができます。 +内部的にメソッドを登録するプロセスを`ContextCircleMenuLoader.OnBuild`でフックすることができます。これを利用して以下のように記述します。 ```cs public class Menu @@ -159,25 +148,98 @@ public class Menu } ``` +## カスタマイズ + + ### ボタンのカスタマイズ ボタンのUIが気に入らなければ、独自のボタンに置き換えることができます。 -`builder.ConfigureFolder();`を使ってください。 +`ContextCircleMenuLoader.OnBuild`内で`builder.ConfigureButton`を使用してください。 -> [!CAUTION] -> これは未完成の機能です。 -> -> 破壊的な変更が加えられる可能性があります。 +```cs +ContextCircleMenuLoader.OnBuild += (builder => +{ + ... + builder.ConfigureButton(FolderMenuFactory); +}); +``` + +独自のボタンを作成するためには、CircleButtonを継承したクラスとそれに対応したFactoryクラスを作成する必要があります。 + +ここでは、SampleのCustomで提供しているものと同様のコードを例として紹介します。 + +SampleはPackage Manager > ContextCircleMenu > Samplesからインポートすることができます。 -まず、`CircleMenu`を継承したFolderMenuを作成します。 -独自のボタンは `CreateButtons` を通して自由に作成してください。 -詳しいコードは `FolderCircleMenu.cs` を参照してください。 +この例では、アイコンのみ表示するボタンに置き換えています。 -次に `IFolderCircleMenuFactory` を実装した FolderMenuFactory を作成します。 -詳しいコードは `CircleMenuFactory` を参照してください。 +以下のクラスを作成してください。 -最後に、以下のようにすることでUIの置き換えが完了します。 +```cs +public class CustomButtonFactory : IButtonFactory +{ + public CircleButton Create(string path, GUIContent icon, Action onSelected, int section) + { + return new OnlyImageCircleButton(path, icon, section, onSelected); + } + + // フォルダ構造のメニューを作成する際に戻るボタンが必要です。 + // ConfigureFolderを利用しない限りsectionは-1にしてください。 + public CircleButton CreateBackButton(Action onBack) + { + return new OnlyImageCircleButton("Back", EditorGUIUtility.IconContent(EditorIcons.Back2x), + -1, onBack); + } +} + +public class OnlyImageCircleButton : CircleButton +{ + public OnlyImageCircleButton(string text, GUIContent icon, int section, Action onSelect) : base(text, icon, section, onSelect) + { + } + + // 生成したボタンを編集することができます。 + // ここで自由にボタンをカスタマイズしてください。 + protected override void ModifierButton(Button button, string text, GUIContent icon, int section) + { + var image = new Image + { + image = icon.image, + style = + { + width = 32f, + height = 32f, + flexShrink = 0 + }, + tooltip = text + }; + + button.Add(image); + } +} + +``` + +作成したFactoryクラスを設定します。 + +```cs +ContextCircleMenuLoader.OnBuild += (builder => +{ + ... + builder.ConfigureButton(new CustomButtonFactory()); +}); +``` + +すると以下のようにアイコンのみ表示するボタンに置き換わります。 + +![alt text](docs/image-6.png) + + +### フォルダのカスタマイズ + +フォルダのUIが気に入らなければ、独自のフォルダに置き換えることができます。 + +`ContextCircleMenuLoader.OnBuild`内で`builder.ConfigureFolder`を使用してください。 ```cs ContextCircleMenuLoader.OnBuild += (builder => @@ -187,7 +249,93 @@ ContextCircleMenuLoader.OnBuild += (builder => }); ``` -## ショートカットキーのカスタマイズ +> [!CAUTION] +> v1.0.0で破壊的な変更が加えられました。 + +独自のフォルダを作成するためには、FolderCircleMenuを継承したクラスとそれに対応したFactoryクラスを作成する必要があります。 + + +ここでは、SampleのCustomで提供しているものと同様のコードを例として紹介します。 + +SampleはPackage Manager > ContextCircleMenu > Samplesからインポートすることができます。 + +この例では、既存のUIにベクターグラフィックスを加えたものに置き換えています。 + +以下のクラスを作成してください。 + +```cs +public class CustomFolderMenuFactory : IFolderCircleMenuFactory +{ + public FolderCircleMenu Create(string path, IMenuControllable menu, CircleMenu parent, IButtonFactory factory) + { + return new CustomFolderCircleMenu(path, menu, parent, factory); + } +} + +public class CustomFolderCircleMenu : FolderCircleMenu +{ + public CustomFolderCircleMenu(string path, IMenuControllable menu, CircleMenu parent, IButtonFactory factory) : + base(path, menu, EditorGUIUtility.IconContent(EditorIcons.FolderIcon), parent, factory) + { + } + + protected override VisualElement[] CreateUtilityElements(ref ContextCircleMenuOption menuOption) + { + var element = new VisualElement(); + var option = menuOption; + element.generateVisualContent += context => + { + var painter = context.painter2D; + var buttonCount = ButtonElements.Length; + for (var i = 0; i < buttonCount; i++) + { + var angle = (float)i / buttonCount * 360f; + if (buttonCount % 2 == 1) + angle += 180f; + else + angle += 180f - 360f / buttonCount / 2; + var vector = new Vector2( + Mathf.Sin(Mathf.Deg2Rad * angle), + Mathf.Cos(Mathf.Deg2Rad * angle)).normalized; + + var from = vector * 12f; + var to = vector * option.Radius * 1.5f; + painter.strokeColor = Color.black; + painter.lineWidth = 2f; + painter.BeginPath(); + painter.MoveTo(from); + painter.LineTo(to); + painter.Stroke(); + } + + painter.BeginPath(); + painter.Arc(Vector2.zero, option.Radius * 1.5f, 0, 360f); + painter.fillColor = new Color(0f, 0f, 0f, 0.2f); + painter.Fill(); + + painter.DrawCircle(Vector2.zero, option.Radius * 1.5f, 0, 360f, 5f, Color.gray); + }; + return new[] { element }; + } +} +``` + +作成したFactoryクラスを設定します。 + +```cs +ContextCircleMenuLoader.OnBuild += (builder => +{ + ... + builder.ConfigureButton(new CustomButtonFactory()); + builder.ConfigureFolder(new CustomFolderMenuFactory()); +}); +``` + +すると以下のようなUIに置き換わります。 + +![alt text](docs/image-5.png) + +### ショートカットキーのカスタマイズ デフォルトのメニュー開閉ボタンは`A`キーに設定されていますが、自由に変更することができます。 @@ -198,6 +346,63 @@ ContextCircleMenuLoader.OnBuild += (builder => 好きなキーに設定してください。 +## API ドキュメント + +ここでは主要なAPIを説明します。UIをカスタマイズする際の参考にしてください。 + +### ContextCircleMenu + +#### プロパティ +| プロパティ名 | 説明 | +| ---- | ---- | +| BlockMouseEvents | trueの場合にクリック等のマウス操作を無効化します | + +#### メソッド +| メソッド名 | 説明 | +| ---- | ---- | +| Show() | メニューを表示します。 | +| Hide() | メニューを非表示にします。 | +| Open(CircleMenu menu) | 引数に指定したメニューを開きます。 | +| Back() | 前回のメニューを開きます。 | +| TryForceSelect() | フォーカスされているボタンがあれば強制的に選択します。 | +| TryForceEnterByMousePosition() | マウスの位置に対応するボタンを強制的にフォーカスします。 | +| CreateMenu(Action\ configureMenu) | メニュー内容をCircleMenuBuilderを用いて作成します。 | + +### CircleMenuBuilder +#### メソッド +| メソッド名 | 説明 | +| ---- | ---- | +| AddMenu(ICircleMenuFactory factory) | 独自のメニューを追加します。 | +| AddMenu(string path, GUIContent content, Action action) | 手動でメニューを追加します。 | +| AddMenu(ContextCircleMenuAttribute attribute, MethodInfo method) | 属性からメニューを追加します。 | +| ConfigureFolder(IFolderCircleMenuFactory factory) | 独自のフォルダーメニューに置き換えます。 | +| ConfigureButton(IButtonFactory factory) | 独自のボタンに置き換えます。 | + +### CircleMenu +#### 抽象メソッド +| メソッド名 | 説明 | +| ---- | ---- | +| CreateButtons(IButtonFactory factory, ref ContextCircleMenuOption menuOption) | メニューに表示するボタンを作成します。ここで渡されるIButtonFactoryはCircleMenuBuilder.ConfigureButton()で設定したものになります。 | + +#### 仮想メソッド +| メソッド名 | 説明 | +| ---- | ---- | +| CreateUtilityElements(ref ContextCircleMenuOption menuOption) | ボタン以外の要素を作成します。 | +| OnInitialized(ref ContextCircleMenuOption menuOption) | 初期化時に呼ばれます。 | +| OnBuild() | 要素作成時に呼ばれます。主にShow()やOpen()が呼ばれた時です。 | + +### CircleButton +#### 抽象メソッド +| メソッド名 | 説明 | +| ---- | ---- | +| ModifierButton(Button button, string text, GUIContent icon, int section) | ボタン作成時に呼ばれます。ボタンの要素を編集するために用いてください。 | + +#### 仮想メソッド +| メソッド名 | 説明 | +| ---- | ---- | +| OnMouseEnter(Button button, MouseEnterEvent evt) | マウスが要素内に入った際に呼ばれます。| +| OnMouseLeave(Button button, MouseLeaveEvent evt) | マウスが要素内から出た際に呼ばれます。 | + ## LISENCE MIT diff --git a/docs/image-5.png b/docs/image-5.png new file mode 100644 index 0000000..b69830e Binary files /dev/null and b/docs/image-5.png differ diff --git a/docs/image-6.png b/docs/image-6.png new file mode 100644 index 0000000..977da2e Binary files /dev/null and b/docs/image-6.png differ