Popup.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using System;
  4. using System.Diagnostics;
  5. using System.Linq;
  6. using System.Reactive.Disposables;
  7. using Avalonia.Controls.Presenters;
  8. using Avalonia.Input;
  9. using Avalonia.Input.Raw;
  10. using Avalonia.Interactivity;
  11. using Avalonia.LogicalTree;
  12. using Avalonia.Metadata;
  13. using Avalonia.VisualTree;
  14. #nullable enable
  15. namespace Avalonia.Controls.Primitives
  16. {
  17. /// <summary>
  18. /// Displays a popup window.
  19. /// </summary>
  20. public class Popup : Control, IVisualTreeHost
  21. {
  22. /// <summary>
  23. /// Defines the <see cref="Child"/> property.
  24. /// </summary>
  25. public static readonly StyledProperty<Control?> ChildProperty =
  26. AvaloniaProperty.Register<Popup, Control?>(nameof(Child));
  27. /// <summary>
  28. /// Defines the <see cref="IsOpen"/> property.
  29. /// </summary>
  30. public static readonly DirectProperty<Popup, bool> IsOpenProperty =
  31. AvaloniaProperty.RegisterDirect<Popup, bool>(
  32. nameof(IsOpen),
  33. o => o.IsOpen,
  34. (o, v) => o.IsOpen = v);
  35. /// <summary>
  36. /// Defines the <see cref="PlacementMode"/> property.
  37. /// </summary>
  38. public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
  39. AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
  40. #pragma warning disable 618
  41. /// <summary>
  42. /// Defines the <see cref="ObeyScreenEdges"/> property.
  43. /// </summary>
  44. public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
  45. AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
  46. #pragma warning restore 618
  47. /// <summary>
  48. /// Defines the <see cref="HorizontalOffset"/> property.
  49. /// </summary>
  50. public static readonly StyledProperty<double> HorizontalOffsetProperty =
  51. AvaloniaProperty.Register<Popup, double>(nameof(HorizontalOffset));
  52. /// <summary>
  53. /// Defines the <see cref="VerticalOffset"/> property.
  54. /// </summary>
  55. public static readonly StyledProperty<double> VerticalOffsetProperty =
  56. AvaloniaProperty.Register<Popup, double>(nameof(VerticalOffset));
  57. /// <summary>
  58. /// Defines the <see cref="PlacementTarget"/> property.
  59. /// </summary>
  60. public static readonly StyledProperty<Control?> PlacementTargetProperty =
  61. AvaloniaProperty.Register<Popup, Control?>(nameof(PlacementTarget));
  62. /// <summary>
  63. /// Defines the <see cref="StaysOpen"/> property.
  64. /// </summary>
  65. public static readonly StyledProperty<bool> StaysOpenProperty =
  66. AvaloniaProperty.Register<Popup, bool>(nameof(StaysOpen), true);
  67. /// <summary>
  68. /// Defines the <see cref="Topmost"/> property.
  69. /// </summary>
  70. public static readonly StyledProperty<bool> TopmostProperty =
  71. AvaloniaProperty.Register<Popup, bool>(nameof(Topmost));
  72. private bool _isOpen;
  73. private bool _ignoreIsOpenChanged;
  74. private PopupOpenState? _openState;
  75. /// <summary>
  76. /// Initializes static members of the <see cref="Popup"/> class.
  77. /// </summary>
  78. static Popup()
  79. {
  80. IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
  81. ChildProperty.Changed.AddClassHandler<Popup>((x, e) => x.ChildChanged(e));
  82. IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e));
  83. }
  84. /// <summary>
  85. /// Raised when the popup closes.
  86. /// </summary>
  87. public event EventHandler? Closed;
  88. /// <summary>
  89. /// Raised when the popup opens.
  90. /// </summary>
  91. public event EventHandler? Opened;
  92. public IPopupHost? Host => _openState?.PopupHost;
  93. /// <summary>
  94. /// Gets or sets the control to display in the popup.
  95. /// </summary>
  96. [Content]
  97. public Control? Child
  98. {
  99. get { return GetValue(ChildProperty); }
  100. set { SetValue(ChildProperty, value); }
  101. }
  102. /// <summary>
  103. /// Gets or sets a dependency resolver for the <see cref="PopupRoot"/>.
  104. /// </summary>
  105. /// <remarks>
  106. /// This property allows a client to customize the behaviour of the popup by injecting
  107. /// a specialized dependency resolver into the <see cref="PopupRoot"/>'s constructor.
  108. /// </remarks>
  109. public IAvaloniaDependencyResolver? DependencyResolver
  110. {
  111. get;
  112. set;
  113. }
  114. /// <summary>
  115. /// Gets or sets a value indicating whether the popup is currently open.
  116. /// </summary>
  117. public bool IsOpen
  118. {
  119. get { return _isOpen; }
  120. set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
  121. }
  122. /// <summary>
  123. /// Gets or sets the placement mode of the popup in relation to the <see cref="PlacementTarget"/>.
  124. /// </summary>
  125. public PlacementMode PlacementMode
  126. {
  127. get { return GetValue(PlacementModeProperty); }
  128. set { SetValue(PlacementModeProperty, value); }
  129. }
  130. [Obsolete("This property has no effect")]
  131. public bool ObeyScreenEdges
  132. {
  133. get => GetValue(ObeyScreenEdgesProperty);
  134. set => SetValue(ObeyScreenEdgesProperty, value);
  135. }
  136. /// <summary>
  137. /// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>
  138. /// </summary>
  139. public double HorizontalOffset
  140. {
  141. get { return GetValue(HorizontalOffsetProperty); }
  142. set { SetValue(HorizontalOffsetProperty, value); }
  143. }
  144. /// <summary>
  145. /// Gets or sets the Vertical offset of the popup in relation to the <see cref="PlacementTarget"/>
  146. /// </summary>
  147. public double VerticalOffset
  148. {
  149. get { return GetValue(VerticalOffsetProperty); }
  150. set { SetValue(VerticalOffsetProperty, value); }
  151. }
  152. /// <summary>
  153. /// Gets or sets the control that is used to determine the popup's position.
  154. /// </summary>
  155. public Control? PlacementTarget
  156. {
  157. get { return GetValue(PlacementTargetProperty); }
  158. set { SetValue(PlacementTargetProperty, value); }
  159. }
  160. /// <summary>
  161. /// Gets or sets a value indicating whether the popup should stay open when the popup is
  162. /// pressed or loses focus.
  163. /// </summary>
  164. public bool StaysOpen
  165. {
  166. get { return GetValue(StaysOpenProperty); }
  167. set { SetValue(StaysOpenProperty, value); }
  168. }
  169. /// <summary>
  170. /// Gets or sets whether this popup appears on top of all other windows
  171. /// </summary>
  172. public bool Topmost
  173. {
  174. get { return GetValue(TopmostProperty); }
  175. set { SetValue(TopmostProperty, value); }
  176. }
  177. /// <summary>
  178. /// Gets the root of the popup window.
  179. /// </summary>
  180. IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot;
  181. /// <summary>
  182. /// Opens the popup.
  183. /// </summary>
  184. public void Open()
  185. {
  186. // Popup is currently open
  187. if (_openState != null)
  188. {
  189. return;
  190. }
  191. var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType<IVisual>().FirstOrDefault();
  192. if (placementTarget == null)
  193. {
  194. throw new InvalidOperationException("Popup has no logical parent and PlacementTarget is null");
  195. }
  196. var topLevel = placementTarget.VisualRoot as TopLevel;
  197. if (topLevel == null)
  198. {
  199. throw new InvalidOperationException(
  200. "Attempted to open a popup not attached to a TopLevel");
  201. }
  202. var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
  203. var handlerCleanup = new CompositeDisposable(5);
  204. void DeferCleanup(IDisposable? disposable)
  205. {
  206. if (disposable is null)
  207. {
  208. return;
  209. }
  210. handlerCleanup.Add(disposable);
  211. }
  212. DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
  213. HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
  214. popupHost.SetChild(Child);
  215. ((ISetLogicalParent)popupHost).SetParent(this);
  216. popupHost.ConfigurePosition(
  217. placementTarget,
  218. PlacementMode,
  219. new Point(HorizontalOffset, VerticalOffset));
  220. DeferCleanup(SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
  221. (x, handler) => x.TemplateApplied += handler,
  222. (x, handler) => x.TemplateApplied -= handler));
  223. if (topLevel is Window window)
  224. {
  225. DeferCleanup(SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated,
  226. (x, handler) => x.Deactivated += handler,
  227. (x, handler) => x.Deactivated -= handler));
  228. }
  229. else
  230. {
  231. var parentPopupRoot = topLevel as PopupRoot;
  232. if (parentPopupRoot?.Parent is Popup popup)
  233. {
  234. DeferCleanup(SubscribeToEventHandler<Popup, EventHandler>(popup, ParentClosed,
  235. (x, handler) => x.Closed += handler,
  236. (x, handler) => x.Closed -= handler));
  237. }
  238. }
  239. DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
  240. DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
  241. var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
  242. {
  243. state.handlerCleanup.Dispose();
  244. state.popupHost.SetChild(null);
  245. state.popupHost.Hide();
  246. ((ISetLogicalParent)state.popupHost).SetParent(null);
  247. state.popupHost.Dispose();
  248. });
  249. _openState = new PopupOpenState(topLevel, popupHost, cleanupPopup);
  250. popupHost.Show();
  251. using (BeginIgnoringIsOpen())
  252. {
  253. IsOpen = true;
  254. }
  255. Opened?.Invoke(this, EventArgs.Empty);
  256. }
  257. /// <summary>
  258. /// Closes the popup.
  259. /// </summary>
  260. public void Close()
  261. {
  262. if (_openState is null)
  263. {
  264. using (BeginIgnoringIsOpen())
  265. {
  266. IsOpen = false;
  267. }
  268. return;
  269. }
  270. _openState.Dispose();
  271. _openState = null;
  272. using (BeginIgnoringIsOpen())
  273. {
  274. IsOpen = false;
  275. }
  276. Closed?.Invoke(this, EventArgs.Empty);
  277. }
  278. /// <summary>
  279. /// Measures the control.
  280. /// </summary>
  281. /// <param name="availableSize">The available size for the control.</param>
  282. /// <returns>A size of 0,0 as Popup itself takes up no space.</returns>
  283. protected override Size MeasureCore(Size availableSize)
  284. {
  285. return new Size();
  286. }
  287. /// <inheritdoc/>
  288. protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
  289. {
  290. base.OnDetachedFromLogicalTree(e);
  291. Close();
  292. }
  293. private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler, Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
  294. {
  295. subscribe(target, handler);
  296. return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
  297. }
  298. /// <summary>
  299. /// Called when the <see cref="IsOpen"/> property changes.
  300. /// </summary>
  301. /// <param name="e">The event args.</param>
  302. private void IsOpenChanged(AvaloniaPropertyChangedEventArgs<bool> e)
  303. {
  304. if (!_ignoreIsOpenChanged)
  305. {
  306. if (e.NewValue.Value)
  307. {
  308. Open();
  309. }
  310. else
  311. {
  312. Close();
  313. }
  314. }
  315. }
  316. /// <summary>
  317. /// Called when the <see cref="Child"/> property changes.
  318. /// </summary>
  319. /// <param name="e">The event args.</param>
  320. private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
  321. {
  322. LogicalChildren.Clear();
  323. ((ISetLogicalParent?)e.OldValue)?.SetParent(null);
  324. if (e.NewValue != null)
  325. {
  326. ((ISetLogicalParent)e.NewValue).SetParent(this);
  327. LogicalChildren.Add((ILogical)e.NewValue);
  328. }
  329. }
  330. private void ListenForNonClientClick(RawInputEventArgs e)
  331. {
  332. var mouse = e as RawPointerEventArgs;
  333. if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
  334. {
  335. Close();
  336. }
  337. }
  338. private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
  339. {
  340. if (!StaysOpen && !IsChildOrThis((IVisual)e.Source))
  341. {
  342. Close();
  343. e.Handled = true;
  344. }
  345. }
  346. private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e)
  347. {
  348. if (_openState is null)
  349. {
  350. return;
  351. }
  352. var popupHost = _openState.PopupHost;
  353. popupHost.TemplateApplied -= RootTemplateApplied;
  354. _openState.SetPresenterSubscription(null);
  355. // If the Popup appears in a control template, then the child controls
  356. // that appear in the popup host need to have their TemplatedParent
  357. // properties set.
  358. if (TemplatedParent != null && popupHost.Presenter != null)
  359. {
  360. popupHost.Presenter.ApplyTemplate();
  361. var presenterSubscription = popupHost.Presenter.GetObservable(ContentPresenter.ChildProperty)
  362. .Subscribe(SetTemplatedParentAndApplyChildTemplates);
  363. _openState.SetPresenterSubscription(presenterSubscription);
  364. }
  365. }
  366. private void SetTemplatedParentAndApplyChildTemplates(IControl control)
  367. {
  368. if (control != null)
  369. {
  370. var templatedParent = TemplatedParent;
  371. if (control.TemplatedParent == null)
  372. {
  373. control.SetValue(TemplatedParentProperty, templatedParent);
  374. }
  375. control.ApplyTemplate();
  376. if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
  377. {
  378. foreach (IControl child in control.VisualChildren)
  379. {
  380. SetTemplatedParentAndApplyChildTemplates(child);
  381. }
  382. }
  383. }
  384. }
  385. private bool IsChildOrThis(IVisual child)
  386. {
  387. if (_openState is null)
  388. {
  389. return false;
  390. }
  391. var popupHost = _openState.PopupHost;
  392. IVisual? root = child.VisualRoot;
  393. while (root is IHostedVisualTreeRoot hostedRoot)
  394. {
  395. if (root == popupHost)
  396. {
  397. return true;
  398. }
  399. root = hostedRoot.Host?.VisualRoot;
  400. }
  401. return false;
  402. }
  403. public bool IsInsidePopup(IVisual visual)
  404. {
  405. if (_openState is null)
  406. {
  407. return false;
  408. }
  409. var popupHost = _openState.PopupHost;
  410. return popupHost != null && ((IVisual)popupHost).IsVisualAncestorOf(visual);
  411. }
  412. public bool IsPointerOverPopup => ((IInputElement?)_openState?.PopupHost)?.IsPointerOver ?? false;
  413. private void WindowDeactivated(object sender, EventArgs e)
  414. {
  415. if (!StaysOpen)
  416. {
  417. Close();
  418. }
  419. }
  420. private void ParentClosed(object sender, EventArgs e)
  421. {
  422. if (!StaysOpen)
  423. {
  424. Close();
  425. }
  426. }
  427. private IgnoreIsOpenScope BeginIgnoringIsOpen()
  428. {
  429. return new IgnoreIsOpenScope(this);
  430. }
  431. private readonly struct IgnoreIsOpenScope : IDisposable
  432. {
  433. private readonly Popup _owner;
  434. public IgnoreIsOpenScope(Popup owner)
  435. {
  436. _owner = owner;
  437. _owner._ignoreIsOpenChanged = true;
  438. }
  439. public void Dispose()
  440. {
  441. _owner._ignoreIsOpenChanged = false;
  442. }
  443. }
  444. private class PopupOpenState : IDisposable
  445. {
  446. private readonly IDisposable _cleanup;
  447. private IDisposable? _presenterCleanup;
  448. public PopupOpenState(TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup)
  449. {
  450. TopLevel = topLevel;
  451. PopupHost = popupHost;
  452. _cleanup = cleanup;
  453. }
  454. public TopLevel TopLevel { get; }
  455. public IPopupHost PopupHost { get; }
  456. public void SetPresenterSubscription(IDisposable? presenterCleanup)
  457. {
  458. _presenterCleanup?.Dispose();
  459. _presenterCleanup = presenterCleanup;
  460. }
  461. public void Dispose()
  462. {
  463. _presenterCleanup?.Dispose();
  464. _cleanup.Dispose();
  465. }
  466. }
  467. }
  468. }