Popup.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  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.Linq;
  5. using Avalonia.Input;
  6. using Avalonia.Input.Raw;
  7. using Avalonia.Interactivity;
  8. using Avalonia.Layout;
  9. using Avalonia.LogicalTree;
  10. using Avalonia.Metadata;
  11. using Avalonia.VisualTree;
  12. namespace Avalonia.Controls.Primitives
  13. {
  14. /// <summary>
  15. /// Displays a popup window.
  16. /// </summary>
  17. public class Popup : Control, IVisualTreeHost
  18. {
  19. /// <summary>
  20. /// Defines the <see cref="Child"/> property.
  21. /// </summary>
  22. public static readonly StyledProperty<Control> ChildProperty =
  23. AvaloniaProperty.Register<Popup, Control>(nameof(Child));
  24. /// <summary>
  25. /// Defines the <see cref="IsOpen"/> property.
  26. /// </summary>
  27. public static readonly DirectProperty<Popup, bool> IsOpenProperty =
  28. AvaloniaProperty.RegisterDirect<Popup, bool>(
  29. nameof(IsOpen),
  30. o => o.IsOpen,
  31. (o, v) => o.IsOpen = v);
  32. /// <summary>
  33. /// Defines the <see cref="PlacementMode"/> property.
  34. /// </summary>
  35. public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
  36. AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
  37. /// <summary>
  38. /// Defines the <see cref="ObeyScreenEdges"/> property.
  39. /// </summary>
  40. public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
  41. AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges));
  42. /// <summary>
  43. /// Defines the <see cref="HorizontalOffset"/> property.
  44. /// </summary>
  45. public static readonly StyledProperty<double> HorizontalOffsetProperty =
  46. AvaloniaProperty.Register<Popup, double>(nameof(HorizontalOffset));
  47. /// <summary>
  48. /// Defines the <see cref="VerticalOffset"/> property.
  49. /// </summary>
  50. public static readonly StyledProperty<double> VerticalOffsetProperty =
  51. AvaloniaProperty.Register<Popup, double>(nameof(VerticalOffset));
  52. /// <summary>
  53. /// Defines the <see cref="PlacementTarget"/> property.
  54. /// </summary>
  55. public static readonly StyledProperty<Control> PlacementTargetProperty =
  56. AvaloniaProperty.Register<Popup, Control>(nameof(PlacementTarget));
  57. /// <summary>
  58. /// Defines the <see cref="StaysOpen"/> property.
  59. /// </summary>
  60. public static readonly StyledProperty<bool> StaysOpenProperty =
  61. AvaloniaProperty.Register<Popup, bool>(nameof(StaysOpen), true);
  62. /// <summary>
  63. /// Defines the <see cref="Topmost"/> property.
  64. /// </summary>
  65. public static readonly StyledProperty<bool> TopmostProperty =
  66. AvaloniaProperty.Register<Popup, bool>(nameof(Topmost));
  67. private bool _isOpen;
  68. private PopupRoot _popupRoot;
  69. private TopLevel _topLevel;
  70. private IDisposable _nonClientListener;
  71. bool _ignoreIsOpenChanged = false;
  72. /// <summary>
  73. /// Initializes static members of the <see cref="Popup"/> class.
  74. /// </summary>
  75. static Popup()
  76. {
  77. IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
  78. ChildProperty.Changed.AddClassHandler<Popup>(x => x.ChildChanged);
  79. IsOpenProperty.Changed.AddClassHandler<Popup>(x => x.IsOpenChanged);
  80. TopmostProperty.Changed.AddClassHandler<Popup>((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue);
  81. }
  82. /// <summary>
  83. /// Raised when the popup closes.
  84. /// </summary>
  85. public event EventHandler Closed;
  86. /// <summary>
  87. /// Raised when the popup opens.
  88. /// </summary>
  89. public event EventHandler Opened;
  90. /// <summary>
  91. /// Raised when the popup root has been created, but before it has been shown.
  92. /// </summary>
  93. public event EventHandler PopupRootCreated;
  94. /// <summary>
  95. /// Gets or sets the control to display in the popup.
  96. /// </summary>
  97. [Content]
  98. public Control Child
  99. {
  100. get { return GetValue(ChildProperty); }
  101. set { SetValue(ChildProperty, value); }
  102. }
  103. /// <summary>
  104. /// Gets or sets a dependency resolver for the <see cref="PopupRoot"/>.
  105. /// </summary>
  106. /// <remarks>
  107. /// This property allows a client to customize the behaviour of the popup by injecting
  108. /// a specialized dependency resolver into the <see cref="PopupRoot"/>'s constructor.
  109. /// </remarks>
  110. public IAvaloniaDependencyResolver DependencyResolver
  111. {
  112. get;
  113. set;
  114. }
  115. /// <summary>
  116. /// Gets or sets a value indicating whether the popup is currently open.
  117. /// </summary>
  118. public bool IsOpen
  119. {
  120. get { return _isOpen; }
  121. set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
  122. }
  123. /// <summary>
  124. /// Gets or sets the placement mode of the popup in relation to the <see cref="PlacementTarget"/>.
  125. /// </summary>
  126. public PlacementMode PlacementMode
  127. {
  128. get { return GetValue(PlacementModeProperty); }
  129. set { SetValue(PlacementModeProperty, value); }
  130. }
  131. /// <summary>
  132. /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary
  133. /// when its opened at a position where it would otherwise overlap the screen edge.
  134. /// </summary>
  135. public bool ObeyScreenEdges
  136. {
  137. get => GetValue(ObeyScreenEdgesProperty);
  138. set => SetValue(ObeyScreenEdgesProperty, value);
  139. }
  140. /// <summary>
  141. /// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>
  142. /// </summary>
  143. public double HorizontalOffset
  144. {
  145. get { return GetValue(HorizontalOffsetProperty); }
  146. set { SetValue(HorizontalOffsetProperty, value); }
  147. }
  148. /// <summary>
  149. /// Gets or sets the Vertical offset of the popup in relation to the <see cref="PlacementTarget"/>
  150. /// </summary>
  151. public double VerticalOffset
  152. {
  153. get { return GetValue(VerticalOffsetProperty); }
  154. set { SetValue(VerticalOffsetProperty, value); }
  155. }
  156. /// <summary>
  157. /// Gets or sets the control that is used to determine the popup's position.
  158. /// </summary>
  159. public Control PlacementTarget
  160. {
  161. get { return GetValue(PlacementTargetProperty); }
  162. set { SetValue(PlacementTargetProperty, value); }
  163. }
  164. /// <summary>
  165. /// Gets the root of the popup window.
  166. /// </summary>
  167. public PopupRoot PopupRoot => _popupRoot;
  168. /// <summary>
  169. /// Gets or sets a value indicating whether the popup should stay open when the popup is
  170. /// pressed or loses focus.
  171. /// </summary>
  172. public bool StaysOpen
  173. {
  174. get { return GetValue(StaysOpenProperty); }
  175. set { SetValue(StaysOpenProperty, value); }
  176. }
  177. /// <summary>
  178. /// Gets or sets whether this popup appears on top of all other windows
  179. /// </summary>
  180. public bool Topmost
  181. {
  182. get { return GetValue(TopmostProperty); }
  183. set { SetValue(TopmostProperty, value); }
  184. }
  185. /// <summary>
  186. /// Gets the root of the popup window.
  187. /// </summary>
  188. IVisual IVisualTreeHost.Root => _popupRoot;
  189. /// <summary>
  190. /// Opens the popup.
  191. /// </summary>
  192. public void Open()
  193. {
  194. if (_popupRoot == null)
  195. {
  196. _popupRoot = new PopupRoot(DependencyResolver)
  197. {
  198. [~ContentControl.ContentProperty] = this[~ChildProperty],
  199. [~WidthProperty] = this[~WidthProperty],
  200. [~HeightProperty] = this[~HeightProperty],
  201. [~MinWidthProperty] = this[~MinWidthProperty],
  202. [~MaxWidthProperty] = this[~MaxWidthProperty],
  203. [~MinHeightProperty] = this[~MinHeightProperty],
  204. [~MaxHeightProperty] = this[~MaxHeightProperty],
  205. };
  206. ((ISetLogicalParent)_popupRoot).SetParent(this);
  207. }
  208. _popupRoot.Position = GetPosition();
  209. if (_topLevel == null && PlacementTarget != null)
  210. {
  211. _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel;
  212. }
  213. if (_topLevel != null)
  214. {
  215. var window = _topLevel as Window;
  216. if (window != null)
  217. {
  218. window.Deactivated += WindowDeactivated;
  219. }
  220. else
  221. {
  222. var parentPopuproot = _topLevel as PopupRoot;
  223. if (parentPopuproot?.Parent is Popup popup)
  224. {
  225. popup.Closed += ParentClosed;
  226. }
  227. }
  228. _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
  229. _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick);
  230. }
  231. PopupRootCreated?.Invoke(this, EventArgs.Empty);
  232. _popupRoot.Show();
  233. if (ObeyScreenEdges)
  234. {
  235. _popupRoot.SnapInsideScreenEdges();
  236. }
  237. _ignoreIsOpenChanged = true;
  238. IsOpen = true;
  239. _ignoreIsOpenChanged = false;
  240. Opened?.Invoke(this, EventArgs.Empty);
  241. }
  242. /// <summary>
  243. /// Closes the popup.
  244. /// </summary>
  245. public void Close()
  246. {
  247. if (_popupRoot != null)
  248. {
  249. if (_topLevel != null)
  250. {
  251. _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
  252. var window = _topLevel as Window;
  253. if (window != null)
  254. window.Deactivated -= WindowDeactivated;
  255. else
  256. {
  257. var parentPopuproot = _topLevel as PopupRoot;
  258. if (parentPopuproot?.Parent is Popup popup)
  259. {
  260. popup.Closed -= ParentClosed;
  261. }
  262. }
  263. _nonClientListener?.Dispose();
  264. _nonClientListener = null;
  265. }
  266. _popupRoot.Hide();
  267. }
  268. IsOpen = false;
  269. Closed?.Invoke(this, EventArgs.Empty);
  270. }
  271. /// <summary>
  272. /// Measures the control.
  273. /// </summary>
  274. /// <param name="availableSize">The available size for the control.</param>
  275. /// <returns>A size of 0,0 as Popup itself takes up no space.</returns>
  276. protected override Size MeasureCore(Size availableSize)
  277. {
  278. return new Size();
  279. }
  280. /// <inheritdoc/>
  281. protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
  282. {
  283. base.OnAttachedToLogicalTree(e);
  284. _topLevel = e.Root as TopLevel;
  285. }
  286. /// <inheritdoc/>
  287. protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
  288. {
  289. base.OnDetachedFromLogicalTree(e);
  290. _topLevel = null;
  291. if (_popupRoot != null)
  292. {
  293. ((ISetLogicalParent)_popupRoot).SetParent(null);
  294. _popupRoot.Dispose();
  295. _popupRoot = null;
  296. }
  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 e)
  303. {
  304. if (!_ignoreIsOpenChanged)
  305. {
  306. if ((bool)e.NewValue)
  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. /// <summary>
  331. /// Gets the position for the popup based on the placement properties.
  332. /// </summary>
  333. /// <returns>The popup's position in screen coordinates.</returns>
  334. protected virtual PixelPoint GetPosition()
  335. {
  336. var result = GetPosition(PlacementTarget ?? this.GetVisualParent<Control>(), PlacementMode, PopupRoot,
  337. HorizontalOffset, VerticalOffset);
  338. return result;
  339. }
  340. internal static PixelPoint GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset)
  341. {
  342. var root = target?.GetVisualRoot();
  343. var mode = root != null ? placement : PlacementMode.Pointer;
  344. var scaling = root?.RenderScaling ?? 1;
  345. switch (mode)
  346. {
  347. case PlacementMode.Pointer:
  348. if (popupRoot != null)
  349. {
  350. var screenOffset = PixelPoint.FromPoint(new Point(horizontalOffset, verticalOffset), scaling);
  351. var mouseOffset = ((IInputRoot)popupRoot)?.MouseDevice?.Position ?? default;
  352. return new PixelPoint(
  353. screenOffset.X + mouseOffset.X,
  354. screenOffset.Y + mouseOffset.Y);
  355. }
  356. return default;
  357. case PlacementMode.Bottom:
  358. return target?.PointToScreen(new Point(0 + horizontalOffset, target.Bounds.Height + verticalOffset)) ?? default;
  359. case PlacementMode.Right:
  360. return target?.PointToScreen(new Point(target.Bounds.Width + horizontalOffset, 0 + verticalOffset)) ?? default;
  361. default:
  362. throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
  363. }
  364. }
  365. private void ListenForNonClientClick(RawInputEventArgs e)
  366. {
  367. var mouse = e as RawMouseEventArgs;
  368. if (!StaysOpen && mouse?.Type == RawMouseEventType.NonClientLeftButtonDown)
  369. {
  370. Close();
  371. }
  372. }
  373. private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
  374. {
  375. if (!StaysOpen)
  376. {
  377. if (!IsChildOrThis((IVisual)e.Source))
  378. {
  379. Close();
  380. e.Handled = true;
  381. }
  382. }
  383. }
  384. private bool IsChildOrThis(IVisual child)
  385. {
  386. IVisual root = child.GetVisualRoot();
  387. while (root is PopupRoot)
  388. {
  389. if (root == PopupRoot) return true;
  390. root = ((PopupRoot)root).Parent.GetVisualRoot();
  391. }
  392. return false;
  393. }
  394. private void WindowDeactivated(object sender, EventArgs e)
  395. {
  396. if (!StaysOpen)
  397. {
  398. Close();
  399. }
  400. }
  401. private void ParentClosed(object sender, EventArgs e)
  402. {
  403. if (!StaysOpen)
  404. {
  405. Close();
  406. }
  407. }
  408. }
  409. }