mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 22:46:38 +08:00
770 lines
23 KiB
C#
770 lines
23 KiB
C#
using System.ComponentModel;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using AtomUI.Controls.Primitives;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Controls.Platform;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Threading;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.Menu;
|
|
|
|
public class MenuInteractionHandlerTests
|
|
{
|
|
static MenuInteractionHandlerTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Menu_Uses_AtomUI_Default_Handler()
|
|
{
|
|
new InspectableMenu().Handler
|
|
.ShouldBeOfType<DefaultMenuInteractionHandler>();
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextMenu_Uses_AtomUI_Default_Handler()
|
|
{
|
|
new InspectableContextMenu().Handler
|
|
.ShouldBeOfType<DefaultMenuInteractionHandler>();
|
|
}
|
|
|
|
[Fact]
|
|
public void MenuFlyoutPresenter_Uses_AtomUI_Default_Handler()
|
|
{
|
|
new InspectableMenuFlyoutPresenter().Handler
|
|
.ShouldBeOfType<DefaultMenuInteractionHandler>();
|
|
}
|
|
|
|
[Fact]
|
|
public void MenuFlyoutPresenter_Preserves_Injected_Handler()
|
|
{
|
|
var handler = new TestCustomMenuInteractionHandler();
|
|
|
|
new InspectableMenuFlyoutPresenter(handler).Handler.ShouldBeSameAs(handler);
|
|
}
|
|
|
|
[Fact]
|
|
public void Menu_Close_Cancels_Pending_Open_Before_Early_Return()
|
|
{
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
var menu = new InspectableMenu(handler) { IsMotionEnabled = false };
|
|
var target = CreateSubMenu("Target");
|
|
using var tree = new MenuTree(menu, false, target);
|
|
|
|
handler.Enter(target);
|
|
runner.Count.ShouldBe(1);
|
|
handler.HasPendingOpen.ShouldBeTrue();
|
|
menu.SetIsOpenForTest(false);
|
|
menu.Close();
|
|
handler.HasPendingOpen.ShouldBeFalse();
|
|
runner.RunAll();
|
|
|
|
target.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Menu_CloseImmediately_Cancels_Pending_Open_Before_Early_Return()
|
|
{
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
var menu = new InspectableMenu(handler) { IsMotionEnabled = false };
|
|
var target = CreateSubMenu("Target");
|
|
using var tree = new MenuTree(menu, false, target);
|
|
|
|
handler.Enter(target);
|
|
runner.Count.ShouldBe(1);
|
|
handler.HasPendingOpen.ShouldBeTrue();
|
|
menu.SetIsOpenForTest(false);
|
|
menu.CloseImmediatelyForTest();
|
|
handler.HasPendingOpen.ShouldBeFalse();
|
|
runner.RunAll();
|
|
|
|
target.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void MenuFlyoutPresenter_Close_Cancels_Pending_Open_Before_Popup_Lookup()
|
|
{
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
var presenter = new InspectableMenuFlyoutPresenter(handler);
|
|
var target = CreateSubMenu("Target");
|
|
using var tree = new MenuTree(presenter, false, target);
|
|
|
|
handler.Enter(target);
|
|
runner.Count.ShouldBe(1);
|
|
handler.HasPendingOpen.ShouldBeTrue();
|
|
presenter.Close();
|
|
handler.HasPendingOpen.ShouldBeFalse();
|
|
runner.RunAll();
|
|
|
|
target.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextMenu_Close_Cancels_Pending_Open_Before_Early_Return()
|
|
{
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
var menu = new InspectableContextMenu(handler);
|
|
var target = CreateSubMenu("Target");
|
|
using var tree = new MenuTree(menu, false, target);
|
|
|
|
handler.Enter(target);
|
|
runner.Count.ShouldBe(1);
|
|
handler.HasPendingOpen.ShouldBeTrue();
|
|
menu.Close();
|
|
handler.HasPendingOpen.ShouldBeFalse();
|
|
runner.RunAll();
|
|
|
|
target.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextMenu_PopupClosing_Cancels_Pending_Open_Before_Forwarding()
|
|
{
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
var menu = new InspectableContextMenu(handler);
|
|
var target = CreateSubMenu("Target");
|
|
using var tree = new MenuTree(menu, false, target);
|
|
|
|
handler.Enter(target);
|
|
runner.Count.ShouldBe(1);
|
|
handler.HasPendingOpen.ShouldBeTrue();
|
|
menu.InvokePopupClosing();
|
|
handler.HasPendingOpen.ShouldBeFalse();
|
|
runner.RunAll();
|
|
|
|
target.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Pending_Open_Does_Not_Run_After_Pointer_Leaves_Target()
|
|
{
|
|
using var tree = CreateNestedSubMenu();
|
|
var target = tree.FirstTarget;
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
handler.AttachTo(tree.Menu);
|
|
|
|
handler.Enter(target);
|
|
handler.Exit(target);
|
|
|
|
runner.RunNext();
|
|
|
|
target.IsSubMenuOpen.ShouldBeFalse();
|
|
runner.RunAll();
|
|
}
|
|
|
|
[Fact]
|
|
public void Pending_Close_Does_Not_Run_After_Pointer_Reenters_Target()
|
|
{
|
|
using var tree = CreateNestedSubMenu();
|
|
var target = tree.FirstTarget;
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
handler.AttachTo(tree.Menu);
|
|
|
|
handler.Enter(target);
|
|
runner.RunNext();
|
|
target.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
handler.Exit(target);
|
|
handler.Enter(target);
|
|
runner.RunNext();
|
|
|
|
target.IsSubMenuOpen.ShouldBeTrue();
|
|
runner.RunAll();
|
|
}
|
|
|
|
[Fact]
|
|
public void Entering_Submenu_Descendant_Invalidates_Ancestor_Pending_Close()
|
|
{
|
|
var descendant = CreateSubMenu("Descendant");
|
|
var target = new MenuItem
|
|
{
|
|
Header = "Target",
|
|
Items =
|
|
{
|
|
descendant
|
|
}
|
|
};
|
|
using var tree = new MenuTree(target);
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
handler.AttachTo(tree.Menu);
|
|
|
|
handler.Enter(target);
|
|
runner.RunNext();
|
|
target.IsSubMenuOpen.ShouldBeTrue();
|
|
Dispatcher.UIThread.RunJobs();
|
|
target.ContainerFromIndex(0).ShouldBeSameAs(descendant);
|
|
|
|
handler.Exit(target);
|
|
handler.Enter(descendant);
|
|
runner.RunAll();
|
|
|
|
target.IsSubMenuOpen.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Entering_Sibling_Submenu_Does_Not_Invalidate_Pending_Close()
|
|
{
|
|
using var tree = CreateSiblingSubMenus();
|
|
var firstTarget = tree.FirstTarget;
|
|
var secondTarget = tree.SecondTarget;
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
handler.AttachTo(tree.Menu);
|
|
|
|
handler.Enter(firstTarget);
|
|
runner.RunNext();
|
|
firstTarget.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
handler.Exit(firstTarget);
|
|
handler.Enter(secondTarget);
|
|
runner.RunAll();
|
|
|
|
firstTarget.IsSubMenuOpen.ShouldBeFalse();
|
|
secondTarget.IsSubMenuOpen.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Keyboard_Right_Invalidates_Pending_Close()
|
|
{
|
|
using var tree = CreateNestedSubMenu();
|
|
var target = tree.FirstTarget;
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
handler.AttachTo(tree.Menu);
|
|
|
|
handler.Enter(target);
|
|
runner.RunNext();
|
|
target.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
handler.Exit(target);
|
|
var args = handler.PressKey(target, Key.Right);
|
|
runner.RunNext();
|
|
|
|
args.Handled.ShouldBeTrue();
|
|
target.IsSubMenuOpen.ShouldBeTrue();
|
|
runner.RunAll();
|
|
}
|
|
|
|
[Fact]
|
|
public void Unhandled_Key_Does_Not_Invalidate_Pending_Open()
|
|
{
|
|
using var tree = CreateNestedSubMenu();
|
|
var target = tree.FirstTarget;
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
handler.AttachTo(tree.Menu);
|
|
|
|
handler.Enter(target);
|
|
var args = handler.PressKey(target, Key.A);
|
|
runner.RunAll();
|
|
|
|
args.Handled.ShouldBeFalse();
|
|
target.IsSubMenuOpen.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Unhandled_Directional_Key_Does_Not_Invalidate_Pending_Open()
|
|
{
|
|
using var tree = CreateNestedSubMenu();
|
|
var target = tree.FirstTarget;
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
handler.AttachTo(tree.Menu);
|
|
|
|
handler.Enter(target);
|
|
var args = handler.PressKey(new Avalonia.Controls.TextBlock(), Key.Right);
|
|
runner.RunAll();
|
|
|
|
args.Handled.ShouldBeFalse();
|
|
target.IsSubMenuOpen.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Pointer_Press_Invalidates_Pending_Close()
|
|
{
|
|
using var tree = CreateNestedSubMenu();
|
|
var target = tree.FirstTarget;
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
handler.AttachTo(tree.Menu);
|
|
|
|
handler.Enter(target);
|
|
runner.RunNext();
|
|
target.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
handler.Exit(target);
|
|
var args = handler.Press(target);
|
|
runner.RunNext();
|
|
|
|
args.Handled.ShouldBeTrue();
|
|
target.IsSubMenuOpen.ShouldBeTrue();
|
|
runner.RunAll();
|
|
}
|
|
|
|
[Fact]
|
|
public void Only_Current_Submenu_Target_Can_Open()
|
|
{
|
|
using var tree = CreateSiblingSubMenus();
|
|
var firstTarget = tree.FirstTarget;
|
|
var secondTarget = tree.SecondTarget;
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
handler.AttachTo(tree.Menu);
|
|
|
|
handler.Enter(firstTarget);
|
|
handler.Enter(secondTarget);
|
|
runner.RunAll();
|
|
|
|
firstTarget.IsSubMenuOpen.ShouldBeFalse();
|
|
secondTarget.IsSubMenuOpen.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Reentering_First_Target_Does_Not_Reactivate_Its_Old_Open_Callback()
|
|
{
|
|
using var tree = CreateSiblingSubMenus();
|
|
var firstTarget = tree.FirstTarget;
|
|
var secondTarget = tree.SecondTarget;
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
handler.AttachTo(tree.Menu);
|
|
|
|
handler.Enter(firstTarget);
|
|
handler.Enter(secondTarget);
|
|
handler.Enter(firstTarget);
|
|
|
|
runner.RunNext();
|
|
firstTarget.IsSubMenuOpen.ShouldBeFalse();
|
|
|
|
runner.RunNext();
|
|
secondTarget.IsSubMenuOpen.ShouldBeFalse();
|
|
|
|
runner.RunNext();
|
|
firstTarget.IsSubMenuOpen.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Pending_Open_Does_Not_Run_After_Handler_Detach()
|
|
{
|
|
using var tree = CreateNestedSubMenu();
|
|
var target = tree.FirstTarget;
|
|
var runner = new QueuedDelayRunner();
|
|
using var handler = new TestMenuInteractionHandler(runner.Schedule);
|
|
|
|
handler.AttachTo(tree.Menu);
|
|
handler.Enter(target);
|
|
handler.HasPendingOpen.ShouldBeTrue();
|
|
handler.DetachFrom(tree.Menu);
|
|
handler.HasPendingOpen.ShouldBeFalse();
|
|
runner.RunAll();
|
|
|
|
target.IsSubMenuOpen.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Throwing_Delay_Runner_Cancels_Queued_Callback()
|
|
{
|
|
Action? queuedCallback = null;
|
|
var expectedException = new InvalidOperationException("Schedule failed.");
|
|
Action<Action, TimeSpan> runner = (callback, _) =>
|
|
{
|
|
queuedCallback = callback;
|
|
throw expectedException;
|
|
};
|
|
var adapterMethod = typeof(DefaultMenuInteractionHandler).GetMethod(
|
|
"AdaptDelayRun",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
adapterMethod.ShouldNotBeNull();
|
|
var adapter = adapterMethod.Invoke(null, [runner])
|
|
.ShouldBeOfType<Func<Action, TimeSpan, IDisposable>>();
|
|
var callbackCommittedState = false;
|
|
|
|
var actualException = Should.Throw<InvalidOperationException>(
|
|
() => adapter(() => callbackCommittedState = true, TimeSpan.Zero));
|
|
queuedCallback.ShouldNotBeNull();
|
|
queuedCallback();
|
|
|
|
actualException.ShouldBeSameAs(expectedException);
|
|
callbackCommittedState.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Disposed_Adapted_Delay_Releases_Original_Callback()
|
|
{
|
|
var runner = new QueuedDelayRunner();
|
|
var adapter = GetAdaptedDelayRunner(runner.Schedule);
|
|
var capturedReference = ScheduleAndDisposeCallback(adapter);
|
|
|
|
runner.Count.ShouldBe(1);
|
|
ForceFullGc();
|
|
|
|
capturedReference.IsAlive.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Synchronous_Delay_Runner_Invokes_Callback_Once()
|
|
{
|
|
var callbackCount = 0;
|
|
var adapter = GetAdaptedDelayRunner((callback, _) =>
|
|
{
|
|
callback();
|
|
callback();
|
|
});
|
|
|
|
using (adapter(() => ++callbackCount, TimeSpan.Zero))
|
|
{
|
|
}
|
|
|
|
callbackCount.ShouldBe(1);
|
|
}
|
|
|
|
private static Func<Action, TimeSpan, IDisposable> GetAdaptedDelayRunner(
|
|
Action<Action, TimeSpan> runner)
|
|
{
|
|
var adapterMethod = typeof(DefaultMenuInteractionHandler).GetMethod(
|
|
"AdaptDelayRun",
|
|
BindingFlags.Static | BindingFlags.NonPublic);
|
|
adapterMethod.ShouldNotBeNull();
|
|
return adapterMethod.Invoke(null, [runner])
|
|
.ShouldBeOfType<Func<Action, TimeSpan, IDisposable>>();
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static WeakReference ScheduleAndDisposeCallback(
|
|
Func<Action, TimeSpan, IDisposable> adapter)
|
|
{
|
|
var capturedObject = new object();
|
|
var capturedReference = new WeakReference(capturedObject);
|
|
var callback = new Action(() => GC.KeepAlive(capturedObject));
|
|
using (adapter(callback, TimeSpan.Zero))
|
|
{
|
|
}
|
|
|
|
return capturedReference;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static void ForceFullGc()
|
|
{
|
|
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true);
|
|
GC.WaitForPendingFinalizers();
|
|
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true);
|
|
}
|
|
|
|
private static MenuTree CreateNestedSubMenu()
|
|
{
|
|
return new MenuTree(CreateSubMenu("Target"));
|
|
}
|
|
|
|
private static MenuTree CreateSiblingSubMenus()
|
|
{
|
|
return new MenuTree(CreateSubMenu("First"), CreateSubMenu("Second"));
|
|
}
|
|
|
|
private static MenuItem CreateSubMenu(string header)
|
|
{
|
|
return new MenuItem
|
|
{
|
|
Header = header,
|
|
Items =
|
|
{
|
|
new MenuItem { Header = $"{header} Child" }
|
|
}
|
|
};
|
|
}
|
|
|
|
private static void SetInteractionHandler(MenuBase menu, IMenuInteractionHandler handler)
|
|
{
|
|
var interactionHandler = typeof(MenuBase).GetField(
|
|
"<InteractionHandler>k__BackingField",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
interactionHandler.ShouldNotBeNull();
|
|
interactionHandler.SetValue(menu, handler);
|
|
}
|
|
|
|
private sealed class InspectableMenu : AtomUI.Desktop.Controls.Menu
|
|
{
|
|
public InspectableMenu()
|
|
{
|
|
}
|
|
|
|
public InspectableMenu(IMenuInteractionHandler handler)
|
|
{
|
|
SetInteractionHandler(this, handler);
|
|
}
|
|
|
|
public IMenuInteractionHandler Handler => InteractionHandler;
|
|
|
|
public void SetIsOpenForTest(bool isOpen)
|
|
{
|
|
IsOpen = isOpen;
|
|
}
|
|
|
|
public void CloseImmediatelyForTest()
|
|
{
|
|
var closeImmediately = typeof(AtomUI.Desktop.Controls.Menu).GetMethod(
|
|
"CloseImmediately",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
closeImmediately.ShouldNotBeNull();
|
|
closeImmediately.Invoke(this, null);
|
|
}
|
|
}
|
|
|
|
private sealed class InspectableContextMenu : ContextMenu
|
|
{
|
|
public InspectableContextMenu()
|
|
{
|
|
}
|
|
|
|
public InspectableContextMenu(IMenuInteractionHandler handler)
|
|
{
|
|
SetInteractionHandler(this, handler);
|
|
}
|
|
|
|
public IMenuInteractionHandler Handler => InteractionHandler;
|
|
|
|
public void InvokePopupClosing()
|
|
{
|
|
var handlePopupClosing = typeof(ContextMenu).GetMethod(
|
|
"HandlePopupClosing",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
handlePopupClosing.ShouldNotBeNull();
|
|
handlePopupClosing.Invoke(this, [null, new CancelEventArgs()]);
|
|
}
|
|
}
|
|
|
|
private sealed class InspectableMenuFlyoutPresenter : MenuFlyoutPresenter
|
|
{
|
|
public InspectableMenuFlyoutPresenter()
|
|
{
|
|
}
|
|
|
|
public InspectableMenuFlyoutPresenter(IMenuInteractionHandler handler)
|
|
: base(handler)
|
|
{
|
|
}
|
|
|
|
public IMenuInteractionHandler Handler => InteractionHandler;
|
|
}
|
|
|
|
private sealed class TestCustomMenuInteractionHandler : IMenuInteractionHandler
|
|
{
|
|
public void Attach(MenuBase menu)
|
|
{
|
|
}
|
|
|
|
public void Detach(MenuBase menu)
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class TestMenuInteractionHandler : DefaultMenuInteractionHandler, IDisposable
|
|
{
|
|
private Avalonia.Controls.MenuBase? _attachedMenu;
|
|
|
|
public TestMenuInteractionHandler(Action<Action, TimeSpan> delayRun)
|
|
: base(false, null, delayRun)
|
|
{
|
|
}
|
|
|
|
public bool HasPendingOpen
|
|
{
|
|
get
|
|
{
|
|
var pendingOpenTarget = typeof(DefaultMenuInteractionHandler).GetField(
|
|
"_pendingOpenTarget",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
pendingOpenTarget.ShouldNotBeNull();
|
|
return pendingOpenTarget.GetValue(this) != null;
|
|
}
|
|
}
|
|
|
|
public void AttachTo(Avalonia.Controls.MenuBase menu)
|
|
{
|
|
((IMenuInteractionHandler)this).Attach(menu);
|
|
_attachedMenu = menu;
|
|
}
|
|
|
|
public void DetachFrom(Avalonia.Controls.MenuBase menu)
|
|
{
|
|
((IMenuInteractionHandler)this).Detach(menu);
|
|
_attachedMenu = null;
|
|
}
|
|
|
|
public void Enter(MenuItem item)
|
|
{
|
|
PointerEntered(item, new RoutedEventArgs { Source = item });
|
|
}
|
|
|
|
public void Exit(MenuItem item)
|
|
{
|
|
PointerExited(item, new RoutedEventArgs { Source = item });
|
|
}
|
|
|
|
public KeyEventArgs PressKey(Avalonia.Controls.Control source, Key key)
|
|
{
|
|
var args = new KeyEventArgs
|
|
{
|
|
RoutedEvent = InputElement.KeyDownEvent,
|
|
Source = source,
|
|
Key = key
|
|
};
|
|
KeyDown(source, args);
|
|
return args;
|
|
}
|
|
|
|
public PointerPressedEventArgs Press(MenuItem item)
|
|
{
|
|
var args = new PointerPressedEventArgs(
|
|
item,
|
|
new Avalonia.Input.Pointer(
|
|
Avalonia.Input.Pointer.GetNextFreeId(),
|
|
PointerType.Mouse,
|
|
true),
|
|
item,
|
|
default,
|
|
0,
|
|
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
|
|
KeyModifiers.None);
|
|
PointerPressed(item, args);
|
|
return args;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_attachedMenu is { } menu)
|
|
{
|
|
((IMenuInteractionHandler)this).Detach(menu);
|
|
_attachedMenu = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class MenuTree : IDisposable
|
|
{
|
|
private readonly Avalonia.Controls.Window _window;
|
|
|
|
public MenuTree(params MenuItem[] targets)
|
|
: this(new AtomUI.Desktop.Controls.Menu { IsMotionEnabled = false }, targets)
|
|
{
|
|
}
|
|
|
|
public MenuTree(MenuBase menu, params MenuItem[] targets)
|
|
: this(menu, true, targets)
|
|
{
|
|
}
|
|
|
|
public MenuTree(MenuBase menu, bool verifyItemContainers, params MenuItem[] targets)
|
|
{
|
|
Targets = targets;
|
|
Parent = new MenuItem { Header = "Parent" };
|
|
foreach (var target in targets)
|
|
{
|
|
Parent.Items.Add(target);
|
|
}
|
|
|
|
Menu = menu;
|
|
Menu.Items.Add(Parent);
|
|
var overlayPanel = new ScopeAwareOverlayLayerPanel
|
|
{
|
|
Width = 320,
|
|
Height = 240
|
|
};
|
|
overlayPanel.Children.Add(Menu);
|
|
var visualLayerManager = new VisualLayerManager
|
|
{
|
|
Child = overlayPanel
|
|
};
|
|
EnablePopupOverlayLayer(visualLayerManager);
|
|
_window = new Avalonia.Controls.Window
|
|
{
|
|
Width = 320,
|
|
Height = 240,
|
|
Content = visualLayerManager
|
|
};
|
|
|
|
_window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
Menu.ApplyTemplate();
|
|
Parent.ApplyTemplate();
|
|
Parent.IsSubMenuOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
if (!verifyItemContainers)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var index = 0; index < targets.Length; index++)
|
|
{
|
|
Parent.ContainerFromIndex(index).ShouldBeSameAs(targets[index]);
|
|
}
|
|
}
|
|
|
|
public MenuBase Menu { get; }
|
|
|
|
public MenuItem Parent { get; }
|
|
|
|
public MenuItem[] Targets { get; }
|
|
|
|
public MenuItem FirstTarget => Targets[0];
|
|
|
|
public MenuItem SecondTarget => Targets[1];
|
|
|
|
public void Dispose()
|
|
{
|
|
_window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
|
|
private static void EnablePopupOverlayLayer(VisualLayerManager visualLayerManager)
|
|
{
|
|
var property = typeof(VisualLayerManager).GetProperty(
|
|
"EnablePopupOverlayLayer",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(visualLayerManager, true);
|
|
}
|
|
}
|
|
|
|
private sealed class QueuedDelayRunner
|
|
{
|
|
private readonly Queue<Action> _callbacks = new();
|
|
|
|
public int Count => _callbacks.Count;
|
|
|
|
public void Schedule(Action callback, TimeSpan delay)
|
|
{
|
|
_callbacks.Enqueue(callback);
|
|
}
|
|
|
|
public void RunNext()
|
|
{
|
|
_callbacks.Dequeue().Invoke();
|
|
}
|
|
|
|
public void RunAll()
|
|
{
|
|
while (_callbacks.Count > 0)
|
|
{
|
|
RunNext();
|
|
}
|
|
}
|
|
}
|
|
}
|