ContextMenuTests.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. using System;
  2. using Avalonia.Controls.Primitives;
  3. using Avalonia.Input;
  4. using Avalonia.Markup.Xaml;
  5. using Avalonia.Platform;
  6. using Avalonia.Rendering;
  7. using Avalonia.Rendering.Composition;
  8. using Avalonia.UnitTests;
  9. using Moq;
  10. using Xunit;
  11. namespace Avalonia.Controls.UnitTests
  12. {
  13. public class ContextMenuTests
  14. {
  15. private Mock<IPopupImpl> popupImpl;
  16. private MouseTestHelper _mouse = new MouseTestHelper();
  17. [Fact]
  18. public void ContextRequested_Opens_ContextMenu()
  19. {
  20. using (Application())
  21. {
  22. var sut = new ContextMenu();
  23. var target = new Panel
  24. {
  25. ContextMenu = sut
  26. };
  27. var window = new Window { Content = target };
  28. window.ApplyStyling();
  29. window.ApplyTemplate();
  30. ((Control)window.Presenter).ApplyTemplate();
  31. int openedCount = 0;
  32. sut.Opened += (sender, args) =>
  33. {
  34. openedCount++;
  35. };
  36. target.RaiseEvent(new ContextRequestedEventArgs());
  37. Assert.True(sut.IsOpen);
  38. Assert.Equal(1, openedCount);
  39. }
  40. }
  41. [Fact]
  42. public void ContextMenu_Is_Opened_When_ContextFlyout_Is_Also_Set()
  43. {
  44. // We have this test for backwards compatability with the code that already sets custom ContextMenu.
  45. using (Application())
  46. {
  47. var sut = new ContextMenu();
  48. var flyout = new Flyout();
  49. var target = new Panel
  50. {
  51. ContextMenu = sut,
  52. ContextFlyout = flyout
  53. };
  54. var window = new Window { Content = target };
  55. window.ApplyStyling();
  56. window.ApplyTemplate();
  57. ((Control)window.Presenter).ApplyTemplate();
  58. target.RaiseEvent(new ContextRequestedEventArgs());
  59. Assert.True(sut.IsOpen);
  60. Assert.False(flyout.IsOpen);
  61. }
  62. }
  63. [Fact]
  64. public void KeyUp_Raised_On_Target_Opens_ContextFlyout()
  65. {
  66. using (Application())
  67. {
  68. var sut = new ContextMenu();
  69. var target = new Panel
  70. {
  71. ContextMenu = sut
  72. };
  73. var contextRequestedCount = 0;
  74. target.AddHandler(Control.ContextRequestedEvent, (s, a) => contextRequestedCount++, Interactivity.RoutingStrategies.Tunnel);
  75. var window = PreparedWindow(target);
  76. window.Show();
  77. target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window });
  78. Assert.True(sut.IsOpen);
  79. Assert.Equal(1, contextRequestedCount);
  80. }
  81. }
  82. [Fact]
  83. public void KeyUp_Raised_On_Flyout_Closes_Opened_ContextMenu()
  84. {
  85. using (Application())
  86. {
  87. var sut = new ContextMenu();
  88. var target = new Panel
  89. {
  90. ContextMenu = sut
  91. };
  92. var window = PreparedWindow(target);
  93. window.Show();
  94. target.RaiseEvent(new ContextRequestedEventArgs());
  95. Assert.True(sut.IsOpen);
  96. sut.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window });
  97. Assert.False(sut.IsOpen);
  98. }
  99. }
  100. [Fact]
  101. public void Opening_Raises_Single_Opened_Event()
  102. {
  103. using (Application())
  104. {
  105. var sut = new ContextMenu();
  106. var target = new Panel
  107. {
  108. ContextMenu = sut
  109. };
  110. var window = new Window { Content = target };
  111. window.ApplyStyling();
  112. window.ApplyTemplate();
  113. ((Control)window.Presenter).ApplyTemplate();
  114. int openedCount = 0;
  115. sut.Opened += (sender, args) =>
  116. {
  117. openedCount++;
  118. };
  119. sut.Open(target);
  120. Assert.Equal(1, openedCount);
  121. }
  122. }
  123. [Fact]
  124. public void Open_Should_Use_Default_Control()
  125. {
  126. using (Application())
  127. {
  128. var sut = new ContextMenu();
  129. var target = new Panel
  130. {
  131. ContextMenu = sut
  132. };
  133. var window = new Window { Content = target };
  134. window.ApplyStyling();
  135. window.ApplyTemplate();
  136. ((Control)window.Presenter).ApplyTemplate();
  137. bool opened = false;
  138. sut.Opened += (sender, args) =>
  139. {
  140. opened = true;
  141. };
  142. sut.Open();
  143. Assert.True(opened);
  144. }
  145. }
  146. [Fact]
  147. public void Open_Should_Raise_Exception_If_AlreadyDetached()
  148. {
  149. using (Application())
  150. {
  151. var sut = new ContextMenu();
  152. var target = new Panel
  153. {
  154. ContextMenu = sut
  155. };
  156. var window = new Window { Content = target };
  157. window.ApplyStyling();
  158. window.ApplyTemplate();
  159. ((Control)window.Presenter).ApplyTemplate();
  160. target.ContextMenu = null;
  161. Assert.ThrowsAny<Exception>(()=> sut.Open());
  162. }
  163. }
  164. [Fact]
  165. public void Closing_Raises_Single_Closed_Event()
  166. {
  167. using (Application())
  168. {
  169. var sut = new ContextMenu();
  170. var target = new Panel
  171. {
  172. ContextMenu = sut
  173. };
  174. var window = new Window { Content = target };
  175. window.ApplyStyling();
  176. window.ApplyTemplate();
  177. ((Control)window.Presenter).ApplyTemplate();
  178. sut.Open(target);
  179. int closedCount = 0;
  180. sut.Closed += (sender, args) =>
  181. {
  182. closedCount++;
  183. };
  184. sut.Close();
  185. Assert.Equal(1, closedCount);
  186. }
  187. }
  188. [Fact]
  189. public void Cancel_Light_Dismiss_Closing_Keeps_Flyout_Open()
  190. {
  191. using (Application())
  192. {
  193. popupImpl.Setup(x => x.Show(true, false)).Verifiable();
  194. popupImpl.Setup(x => x.Hide()).Verifiable();
  195. var window = PreparedWindow();
  196. window.Width = 100;
  197. window.Height = 100;
  198. var button = new Button
  199. {
  200. Height = 10,
  201. Width = 10,
  202. HorizontalAlignment = Layout.HorizontalAlignment.Left,
  203. VerticalAlignment = Layout.VerticalAlignment.Top
  204. };
  205. window.Content = button;
  206. window.ApplyTemplate();
  207. window.Show();
  208. var tracker = 0;
  209. var c = new ContextMenu();
  210. c.Closing += (s, e) =>
  211. {
  212. tracker++;
  213. e.Cancel = true;
  214. };
  215. button.ContextMenu = c;
  216. c.Open(button);
  217. var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
  218. _mouse.Down(overlay, MouseButton.Left, new Point(90, 90));
  219. _mouse.Up(button, MouseButton.Left, new Point(90, 90));
  220. Assert.Equal(1, tracker);
  221. Assert.True(c.IsOpen);
  222. popupImpl.Verify(x => x.Hide(), Times.Never);
  223. popupImpl.Verify(x => x.Show(true, false), Times.Exactly(1));
  224. }
  225. }
  226. [Fact]
  227. public void Light_Dismiss_Closes_Flyout()
  228. {
  229. using (Application())
  230. {
  231. popupImpl.Setup(x => x.Show(true, false)).Verifiable();
  232. popupImpl.Setup(x => x.Hide()).Verifiable();
  233. var window = PreparedWindow();
  234. window.Width = 100;
  235. window.Height = 100;
  236. var button = new Button
  237. {
  238. Height = 10,
  239. Width = 10,
  240. HorizontalAlignment = Layout.HorizontalAlignment.Left,
  241. VerticalAlignment = Layout.VerticalAlignment.Top
  242. };
  243. window.Content = button;
  244. window.ApplyTemplate();
  245. window.Show();
  246. var c = new ContextMenu();
  247. c.Placement = PlacementMode.Bottom;
  248. c.Open(button);
  249. var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
  250. _mouse.Down(overlay, MouseButton.Left, new Point(90, 90));
  251. _mouse.Up(button, MouseButton.Left, new Point(90, 90));
  252. Assert.False(c.IsOpen);
  253. popupImpl.Verify(x => x.Hide(), Times.Exactly(1));
  254. popupImpl.Verify(x => x.Show(true, false), Times.Exactly(1));
  255. }
  256. }
  257. [Fact]
  258. public void Clicking_On_Control_Toggles_ContextMenu()
  259. {
  260. using (Application())
  261. {
  262. popupImpl.Setup(x => x.Show(true, false)).Verifiable();
  263. popupImpl.Setup(x => x.Hide()).Verifiable();
  264. var sut = new ContextMenu();
  265. var target = new Panel
  266. {
  267. ContextMenu = sut
  268. };
  269. var window = PreparedWindow(target);
  270. window.Show();
  271. var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
  272. _mouse.Click(target, MouseButton.Right);
  273. Assert.True(sut.IsOpen);
  274. _mouse.Down(overlay);
  275. _mouse.Up(target);
  276. Assert.False(sut.IsOpen);
  277. popupImpl.Verify(x => x.Show(true, false), Times.Once);
  278. popupImpl.Verify(x => x.Hide(), Times.Once);
  279. }
  280. }
  281. [Fact]
  282. public void Right_Clicking_On_Control_Twice_Re_Opens_ContextMenu()
  283. {
  284. using (Application())
  285. {
  286. popupImpl.Setup(x => x.Show(true, false)).Verifiable();
  287. popupImpl.Setup(x => x.Hide()).Verifiable();
  288. var sut = new ContextMenu();
  289. var target = new Panel
  290. {
  291. ContextMenu = sut
  292. };
  293. var window = PreparedWindow(target);
  294. window.Show();
  295. var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
  296. _mouse.Click(target, MouseButton.Right);
  297. Assert.True(sut.IsOpen);
  298. _mouse.Down(overlay, MouseButton.Right);
  299. _mouse.Up(target, MouseButton.Right);
  300. Assert.True(sut.IsOpen);
  301. popupImpl.Verify(x => x.Hide(), Times.Once);
  302. popupImpl.Verify(x => x.Show(true, false), Times.Exactly(2));
  303. }
  304. }
  305. [Fact]
  306. public void Context_Menu_Can_Be_Shared_Between_Controls_Even_After_A_Control_Is_Removed_From_Visual_Tree()
  307. {
  308. using (Application())
  309. {
  310. var sut = new ContextMenu();
  311. var target1 = new Panel
  312. {
  313. ContextMenu = sut
  314. };
  315. var target2 = new Panel
  316. {
  317. ContextMenu = sut
  318. };
  319. var sp = new StackPanel { Children = { target1, target2 } };
  320. var window = new Window { Content = sp };
  321. window.ApplyStyling();
  322. window.ApplyTemplate();
  323. ((Control)window.Presenter).ApplyTemplate();
  324. _mouse.Click(target1, MouseButton.Right);
  325. Assert.True(sut.IsOpen);
  326. sp.Children.Remove(target1);
  327. Assert.False(sut.IsOpen);
  328. _mouse.Click(target2, MouseButton.Right);
  329. Assert.True(sut.IsOpen);
  330. }
  331. }
  332. [Fact]
  333. public void Cancelling_Opening_Does_Not_Show_ContextMenu()
  334. {
  335. using (Application())
  336. {
  337. popupImpl.Setup(x => x.Show(true, false)).Verifiable();
  338. bool eventCalled = false;
  339. var sut = new ContextMenu();
  340. var target = new Panel
  341. {
  342. ContextMenu = sut
  343. };
  344. new Window { Content = target };
  345. sut.Opening += (c, e) => { eventCalled = true; e.Cancel = true; };
  346. _mouse.Click(target, MouseButton.Right);
  347. Assert.True(eventCalled);
  348. Assert.False(sut.IsOpen);
  349. popupImpl.Verify(x => x.Show(true, false), Times.Never);
  350. }
  351. }
  352. [Fact]
  353. public void Can_Set_Clear_ContextMenu_Property()
  354. {
  355. using (Application())
  356. {
  357. var target = new ContextMenu();
  358. var control = new Panel();
  359. control.ContextMenu = target;
  360. control.ContextMenu = null;
  361. }
  362. }
  363. [Fact]
  364. public void Should_Reset_Popup_Parent_On_Target_Detached()
  365. {
  366. using (Application())
  367. {
  368. var userControl = new UserControl();
  369. var window = PreparedWindow(userControl);
  370. window.Show();
  371. var menu = new ContextMenu();
  372. userControl.ContextMenu = menu;
  373. menu.Open();
  374. var popup = Assert.IsType<Popup>(menu.Parent);
  375. Assert.NotNull(popup.Parent);
  376. window.Content = null;
  377. Assert.Null(popup.Parent);
  378. }
  379. }
  380. [Fact]
  381. public void Context_Menu_In_Resources_Can_Be_Shared()
  382. {
  383. using (Application())
  384. {
  385. var xaml = @"
  386. <Window xmlns='https://github.com/avaloniaui'
  387. xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
  388. <Window.Resources>
  389. <ContextMenu x:Key='contextMenu'>
  390. <MenuItem>Foo</MenuItem>
  391. </ContextMenu>
  392. </Window.Resources>
  393. <StackPanel>
  394. <TextBlock Name='target1' ContextMenu='{StaticResource contextMenu}'/>
  395. <TextBlock Name='target2' ContextMenu='{StaticResource contextMenu}'/>
  396. </StackPanel>
  397. </Window>";
  398. var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
  399. var target1 = window.Find<TextBlock>("target1");
  400. var target2 = window.Find<TextBlock>("target2");
  401. var mouse = new MouseTestHelper();
  402. Assert.NotNull(target1.ContextMenu);
  403. Assert.NotNull(target2.ContextMenu);
  404. Assert.Same(target1.ContextMenu, target2.ContextMenu);
  405. window.Show();
  406. var menu = target1.ContextMenu;
  407. mouse.Click(target1, MouseButton.Right);
  408. Assert.True(menu.IsOpen);
  409. mouse.Click(target2, MouseButton.Right);
  410. Assert.True(menu.IsOpen);
  411. }
  412. }
  413. [Fact]
  414. public void Context_Menu_Can_Be_Set_In_Style()
  415. {
  416. using (Application())
  417. {
  418. var xaml = @"
  419. <Window xmlns='https://github.com/avaloniaui'
  420. xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
  421. <Window.Styles>
  422. <Style Selector='TextBlock'>
  423. <Setter Property='ContextMenu'>
  424. <ContextMenu>
  425. <MenuItem>Foo</MenuItem>
  426. </ContextMenu>
  427. </Setter>
  428. </Style>
  429. </Window.Styles>
  430. <StackPanel>
  431. <TextBlock Name='target1'/>
  432. <TextBlock Name='target2'/>
  433. </StackPanel>
  434. </Window>";
  435. var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
  436. var target1 = window.Find<TextBlock>("target1");
  437. var target2 = window.Find<TextBlock>("target2");
  438. var mouse = new MouseTestHelper();
  439. Assert.NotNull(target1.ContextMenu);
  440. Assert.NotNull(target2.ContextMenu);
  441. Assert.Same(target1.ContextMenu, target2.ContextMenu);
  442. window.Show();
  443. var menu = target1.ContextMenu;
  444. mouse.Click(target1, MouseButton.Right);
  445. Assert.True(menu.IsOpen);
  446. mouse.Click(target2, MouseButton.Right);
  447. Assert.True(menu.IsOpen);
  448. }
  449. }
  450. [Fact]
  451. public void Cancelling_Closing_Leaves_ContextMenuOpen()
  452. {
  453. using (Application())
  454. {
  455. popupImpl.Setup(x => x.Show(true, false)).Verifiable();
  456. popupImpl.Setup(x => x.Hide()).Verifiable();
  457. bool eventCalled = false;
  458. var sut = new ContextMenu();
  459. var target = new Panel
  460. {
  461. ContextMenu = sut
  462. };
  463. var window = PreparedWindow(target);
  464. var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
  465. sut.Closing += (c, e) => { eventCalled = true; e.Cancel = true; };
  466. window.Show();
  467. _mouse.Click(target, MouseButton.Right);
  468. Assert.True(sut.IsOpen);
  469. _mouse.Down(overlay, MouseButton.Right);
  470. _mouse.Up(target, MouseButton.Right);
  471. Assert.True(eventCalled);
  472. Assert.True(sut.IsOpen);
  473. popupImpl.Verify(x => x.Show(true, false), Times.Once());
  474. popupImpl.Verify(x => x.Hide(), Times.Never);
  475. }
  476. }
  477. [Fact]
  478. public void Closing_Should_Restore_Focus()
  479. {
  480. using (Application())
  481. {
  482. popupImpl.Setup(x => x.Show(true, false)).Verifiable();
  483. popupImpl.Setup(x => x.Hide()).Verifiable();
  484. var item = new MenuItem();
  485. var sut = new ContextMenu
  486. {
  487. Items = { item }
  488. };
  489. var button = new Button();
  490. var target = new Panel
  491. {
  492. Children =
  493. {
  494. button,
  495. },
  496. ContextMenu = sut
  497. };
  498. var window = PreparedWindow(target);
  499. var focusManager = Assert.IsType<FocusManager>(window.FocusManager);
  500. // Show the window and focus the button.
  501. window.Show();
  502. button.Focus();
  503. Assert.Same(button, focusManager.GetFocusedElement());
  504. // Click to show the context menu.
  505. _mouse.Click(target, MouseButton.Right);
  506. Assert.True(sut.IsOpen);
  507. // Hover over the context menu item: this should focus it.
  508. _mouse.Enter(item);
  509. Assert.Same(item, focusManager.GetFocusedElement());
  510. // Click the menu item to close the menu.
  511. _mouse.Click(item);
  512. Assert.False(sut.IsOpen);
  513. // Focus should be restored to the button.
  514. Assert.Same(button, focusManager.GetFocusedElement());
  515. }
  516. }
  517. private static Window PreparedWindow(object content = null)
  518. {
  519. var platform = AvaloniaLocator.Current.GetRequiredService<IWindowingPlatform>();
  520. var windowImpl = Mock.Get(platform.CreateWindow());
  521. windowImpl.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
  522. var w = new Window(windowImpl.Object) { Content = content };
  523. w.ApplyStyling();
  524. w.ApplyTemplate();
  525. ((Control)w.Presenter).ApplyTemplate();
  526. return w;
  527. }
  528. private IDisposable Application()
  529. {
  530. var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100));
  531. var screenImpl = new Mock<IScreenImpl>();
  532. screenImpl.Setup(x => x.ScreenCount).Returns(1);
  533. screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(1, screen, screen, true) });
  534. var windowImpl = MockWindowingPlatform.CreateWindowMock();
  535. popupImpl = MockWindowingPlatform.CreatePopupMock(windowImpl.Object);
  536. popupImpl.SetupGet(x => x.RenderScaling).Returns(1);
  537. windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object);
  538. windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screenImpl.Object);
  539. var services = TestServices.StyledWindow.With(
  540. focusManager: new FocusManager(),
  541. keyboardDevice: () => new KeyboardDevice(),
  542. inputManager: new InputManager(),
  543. windowImpl: windowImpl.Object,
  544. windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, x => popupImpl.Object));
  545. return UnitTestApplication.Start(services);
  546. }
  547. }
  548. }