Code/XGUI/Elements/ContextMenu.cs
using Sandbox;
using Sandbox.UI;
using Sandbox.UI.Construct;
using System;
using System.Collections.Generic;
namespace XGUI
{
/// <summary>
/// A Windows 95-style expandable context menu that supports hierarchical items
/// </summary>
public class ContextMenu : XGUIPopup
{
/// <summary>
/// The parent menu if this is a submenu
/// </summary>
public ContextMenu ParentMenu { get; private set; }
/// <summary>
/// The currently open child submenu, if any
/// </summary>
public ContextMenu ActiveSubmenu { get; private set; }
/// <summary>
/// Delay before showing submenu on hover in seconds
/// </summary>
public float SubmenuDelay { get; set; } = 0.3f;
private TimeSince _hoverTime = 0;
private Panel _hoveredItem = null;
private bool _openingSubmenu = false;
// Dictionary to store submenu populate actions
private Dictionary<Panel, Action<ContextMenu>> _submenuActions = new();
public ContextMenu() : base()
{
AddClass( "ContextMenu" );
AcceptsFocus = true;
}
public ContextMenu( Panel sourcePanel, PositionMode position = PositionMode.BelowLeft, float offset = 0 )
: base( sourcePanel, position, offset )
{
AddClass( "ContextMenu" );
}
/// <summary>
/// Add a standard menu item
/// </summary>
public Panel AddMenuItem( string text, Action action = null, string iconname = "", string iconurl = "" )
{
var item = Add.Panel( "MenuItem" );
if ( !String.IsNullOrWhiteSpace( iconname ) )
item.Add.Icon( iconname, "ItemIcon" );
if ( !String.IsNullOrWhiteSpace( iconurl ) )
item.Add.Image( iconurl, "ItemIcon" );
else
item.Add.Image( "", "ItemIcon" );
item.Add.Label( text, "ItemText" );
if ( action != null )
{
item.AddEventListener( "onclick", () =>
{
action?.Invoke();
Success();
} );
}
item.AddEventListener( "onmouseover", () =>
{
if ( !_openingSubmenu )
{
CloseActiveSubmenu();
}
} );
return item;
}
public Panel AddCheckItem( string text, Func<bool> isChecked, Action onClick, string iconurl = "" )
{
var item = Add.Panel( "MenuItem" );
// Add checkmark or empty space in ItemIcon
var icon = item.Add.Label( "", "CheckIcon" );
// Set checkmark state
void UpdateCheck()
{
icon.SetClass( "checked", isChecked() );
}
UpdateCheck();
item.Add.Label( text, "ItemText" );
item.AddEventListener( "onclick", () =>
{
onClick?.Invoke();
UpdateCheck();
Success();
} );
item.AddEventListener( "onmouseover", () =>
{
if ( !_openingSubmenu )
CloseActiveSubmenu();
} );
return item;
}
public Panel AddRadioItem( string text, Func<bool> isSelected, Action onClick, string iconurl = "" )
{
var item = Add.Panel( "MenuItem" );
var icon = item.Add.Label( "", "RadioIcon" );
void UpdateRadio()
{
icon.SetClass( "selected", isSelected() );
}
UpdateRadio();
item.Add.Label( text, "ItemText" );
item.AddEventListener( "onclick", () =>
{
onClick?.Invoke();
UpdateRadio();
Success();
} );
item.AddEventListener( "onmouseover", () =>
{
if ( !_openingSubmenu )
CloseActiveSubmenu();
} );
return item;
}
/// <summary>
/// Add a submenu item that expands when hovered
/// </summary>
public Panel AddSubmenuItem( string text, Action<ContextMenu> populateSubmenu, string iconname = "", string iconurl = "" )
{
var item = Add.Panel( "MenuItem SubmenuItem" );
if ( !String.IsNullOrWhiteSpace( iconname ) )
item.Add.Icon( iconname, "ItemIcon" );
if ( !String.IsNullOrWhiteSpace( iconurl ) )
item.Add.Image( iconurl, "ItemIcon" );
else
item.Add.Image( "", "ItemIcon" );
item.Add.Label( text, "ItemText" );
item.Add.Icon( "arrow_right", "ItemArrow" );
// Track mouse hover for expanding submenu
item.AddEventListener( "onmouseover", () =>
{
// Only handle mouse enter if we're not already opening a submenu
if ( !_openingSubmenu )
{
CloseActiveSubmenu();
_hoveredItem = item;
_hoverTime = 0;
if ( _submenuActions.TryGetValue( _hoveredItem, out var populateAction ) )
{
OpenSubmenu( _hoveredItem, populateAction );
_hoveredItem = null; // Reset to prevent reopening
}
}
} );
item.AddEventListener( "onmouseleave", () =>
{
if ( _hoveredItem == item )
_hoveredItem = null;
} );
// Store the populate action in our dictionary
_submenuActions[item] = populateSubmenu;
return item;
}
/// <summary>
/// Add a separator line
/// </summary>
public Panel AddSeparator()
{
return Add.Panel( "menu-separator" );
}
/// <summary>
/// Open a submenu from the given menu item
/// </summary>
public ContextMenu OpenSubmenu( Panel menuItem, Action<ContextMenu> populateFunc )
{
// Prevent re-entry into submenu opening
if ( _openingSubmenu || ActiveSubmenu != null )
return ActiveSubmenu;
_openingSubmenu = true;
CloseActiveSubmenu();
var submenu = new ContextMenu( menuItem, PositionMode.Right, 0 );
submenu.ParentMenu = this;
populateFunc?.Invoke( submenu );
ActiveSubmenu = submenu;
_openingSubmenu = false;
// Add "open" class to the menu item that has an open submenu
menuItem.AddClass( "open" );
return submenu;
}
public void CloseActiveSubmenu()
{
if ( ActiveSubmenu != null )
{
// Find the parent menu item that opened this submenu and remove its "open" class
foreach ( var child in Children )
{
if ( child.HasClass( "SubmenuItem" ) && child.HasClass( "open" ) )
{
child.RemoveClass( "open" );
}
}
ActiveSubmenu.CloseActiveSubmenu();
ActiveSubmenu.Delete( true );
ActiveSubmenu = null;
}
}
public override void Tick()
{
base.Tick();
// Close menu when clicking outside
if ( Input.Pressed( "attack1" ) && !IsMouseOver() )
{
Popup.CloseAll();
}
}
private bool IsMouseOver()
{
// Check if mouse is over this menu or any active submenu
if ( IsInside( Mouse.Position ) )
return true;
return ActiveSubmenu?.IsMouseOver() ?? false;
}
public override void OnDeleted()
{
// Clear references when menu is deleted
_submenuActions.Clear();
CloseActiveSubmenu();
base.OnDeleted();
}
}
}