View.Hierarchy.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. #nullable enable
  2. using System.Collections.ObjectModel;
  3. using System.Diagnostics;
  4. using System.Diagnostics.CodeAnalysis;
  5. namespace Terminal.Gui;
  6. public partial class View // SuperView/SubView hierarchy management (SuperView, SubViews, Add, Remove, etc.)
  7. {
  8. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  9. private static readonly IReadOnlyCollection<View> _empty = [];
  10. private readonly List<View>? _subviews = [];
  11. // Internally, we use InternalSubViews rather than subviews, as we do not expect us
  12. // to make the same mistakes our users make when they poke at the SubViews.
  13. internal IList<View> InternalSubViews => _subviews ?? [];
  14. /// <summary>Gets the list of SubViews.</summary>
  15. /// <remarks>
  16. /// Use <see cref="Add(Terminal.Gui.View?)"/> and <see cref="Remove(Terminal.Gui.View?)"/> to add or remove subviews.
  17. /// </remarks>
  18. public IReadOnlyCollection<View> SubViews => InternalSubViews?.AsReadOnly () ?? _empty;
  19. private View? _superView;
  20. /// <summary>
  21. /// Gets this Views SuperView (the View's container), or <see langword="null"/> if this view has not been added as a
  22. /// SubView.
  23. /// </summary>
  24. /// <seealso cref="OnSuperViewChanged"/>
  25. /// <seealso cref="SuperViewChanged"/>
  26. public View? SuperView
  27. {
  28. get => _superView!;
  29. private set => SetSuperView (value);
  30. }
  31. private void SetSuperView (View? value)
  32. {
  33. if (_superView == value)
  34. {
  35. return;
  36. }
  37. _superView = value;
  38. RaiseSuperViewChanged ();
  39. }
  40. private void RaiseSuperViewChanged ()
  41. {
  42. SuperViewChangedEventArgs args = new (SuperView, this);
  43. OnSuperViewChanged (args);
  44. SuperViewChanged?.Invoke (this, args);
  45. }
  46. /// <summary>
  47. /// Called when the SuperView of this View has changed.
  48. /// </summary>
  49. /// <param name="e"></param>
  50. protected virtual void OnSuperViewChanged (SuperViewChangedEventArgs e) { }
  51. /// <summary>Raised when the SuperView of this View has changed.</summary>
  52. public event EventHandler<SuperViewChangedEventArgs>? SuperViewChanged;
  53. #region AddRemove
  54. /// <summary>Adds a SubView (child) to this view.</summary>
  55. /// <remarks>
  56. /// <para>
  57. /// The Views that have been added to this view can be retrieved via the <see cref="SubViews"/> property.
  58. /// </para>
  59. /// <para>
  60. /// To check if a View has been added to this View, compare it's <see cref="SuperView"/> property to this View.
  61. /// </para>
  62. /// <para>
  63. /// SubViews will be disposed when this View is disposed. In other-words, calling this method causes
  64. /// the lifecycle of the subviews to be transferred to this View.
  65. /// </para>
  66. /// <para>
  67. /// Calls/Raises the <see cref="OnSubViewAdded"/>/<see cref="SubViewAdded"/> event.
  68. /// </para>
  69. /// <para>
  70. /// The <see cref="OnSuperViewChanged"/>/<see cref="SuperViewChanged"/> event will be raised on the added View.
  71. /// </para>
  72. /// </remarks>
  73. /// <param name="view">The view to add.</param>
  74. /// <returns>The view that was added.</returns>
  75. /// <seealso cref="Remove(View)"/>
  76. /// <seealso cref="RemoveAll"/>
  77. /// <seealso cref="OnSubViewAdded"/>
  78. /// <seealso cref="SubViewAdded"/>
  79. public virtual View? Add (View? view)
  80. {
  81. if (view is null)
  82. {
  83. return null;
  84. }
  85. //Debug.Assert (view.SuperView is null, $"{view} already has a SuperView: {view.SuperView}.");
  86. if (view.SuperView is {})
  87. {
  88. Logging.Warning ($"{view} already has a SuperView: {view.SuperView}.");
  89. }
  90. //Debug.Assert (!InternalSubViews.Contains (view), $"{view} has already been Added to {this}.");
  91. if (InternalSubViews.Contains (view))
  92. {
  93. Logging.Warning ($"{view} has already been Added to {this}.");
  94. }
  95. // TileView likes to add views that were previously added and have HasFocus = true. No bueno.
  96. view.HasFocus = false;
  97. // TODO: Make this thread safe
  98. InternalSubViews.Add (view);
  99. view.SuperView = this;
  100. if (view is { Enabled: true, Visible: true, CanFocus: true })
  101. {
  102. // Add will cause the newly added subview to gain focus if it's focusable
  103. if (HasFocus)
  104. {
  105. view.SetFocus ();
  106. }
  107. }
  108. if (view.Enabled && !Enabled)
  109. {
  110. view.Enabled = false;
  111. }
  112. // Raise event indicating a subview has been added
  113. // We do this before Init.
  114. RaiseSubViewAdded (view);
  115. if (IsInitialized && !view.IsInitialized)
  116. {
  117. view.BeginInit ();
  118. view.EndInit ();
  119. }
  120. SetNeedsDraw ();
  121. SetNeedsLayout ();
  122. return view;
  123. }
  124. /// <summary>Adds the specified SubView (children) to the view.</summary>
  125. /// <param name="views">Array of one or more views (can be optional parameter).</param>
  126. /// <remarks>
  127. /// <para>
  128. /// The Views that have been added to this view can be retrieved via the <see cref="SubViews"/> property. See also
  129. /// <seealso cref="Remove(View)"/> and <seealso cref="RemoveAll"/>.
  130. /// </para>
  131. /// <para>
  132. /// SubViews will be disposed when this View is disposed. In other-words, calling this method causes
  133. /// the lifecycle of the subviews to be transferred to this View.
  134. /// </para>
  135. /// </remarks>
  136. public void Add (params View []? views)
  137. {
  138. if (views is null)
  139. {
  140. return;
  141. }
  142. foreach (View view in views)
  143. {
  144. Add (view);
  145. }
  146. }
  147. internal void RaiseSubViewAdded (View view)
  148. {
  149. OnSubViewAdded (view);
  150. SubViewAdded?.Invoke (this, new (this, view));
  151. }
  152. /// <summary>
  153. /// Called when a SubView has been added to this View.
  154. /// </summary>
  155. /// <remarks>
  156. /// If the SubView has not been initialized, this happens before BeginInit/EndInit is called.
  157. /// </remarks>
  158. /// <param name="view"></param>
  159. protected virtual void OnSubViewAdded (View view) { }
  160. /// <summary>Raised when a SubView has been added to this View.</summary>
  161. /// <remarks>
  162. /// If the SubView has not been initialized, this happens before BeginInit/EndInit is called.
  163. /// </remarks>
  164. public event EventHandler<SuperViewChangedEventArgs>? SubViewAdded;
  165. /// <summary>Removes a SubView added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.</summary>
  166. /// <remarks>
  167. /// <para>
  168. /// Normally SubViews will be disposed when this View is disposed. Removing a SubView causes ownership of the
  169. /// SubView's
  170. /// lifecycle to be transferred to the caller; the caller must call <see cref="Dispose()"/>.
  171. /// </para>
  172. /// <para>
  173. /// Calls/Raises the <see cref="OnSubViewRemoved"/>/<see cref="SubViewRemoved"/> event.
  174. /// </para>
  175. /// <para>
  176. /// The <see cref="OnSuperViewChanged"/>/<see cref="SuperViewChanged"/> event will be raised on the removed View.
  177. /// </para>
  178. /// </remarks>
  179. /// <returns>
  180. /// The removed View. <see langword="null"/> if the View could not be removed.
  181. /// </returns>
  182. /// <seealso cref="OnSubViewRemoved"/>
  183. /// <seealso cref="SubViewRemoved"/>"/>
  184. public virtual View? Remove (View? view)
  185. {
  186. if (view is null)
  187. {
  188. return null;
  189. }
  190. if (InternalSubViews.Count == 0)
  191. {
  192. return view;
  193. }
  194. if (view.SuperView is null)
  195. {
  196. Logging.Warning ($"{view} cannot be Removed. SuperView is null.");
  197. }
  198. if (view.SuperView != this)
  199. {
  200. Logging.Warning ($"{view} cannot be Removed. SuperView is not this ({view.SuperView}.");
  201. }
  202. if (!InternalSubViews.Contains (view))
  203. {
  204. Logging.Warning ($"{view} cannot be Removed. It has not been added to {this}.");
  205. }
  206. Rectangle touched = view.Frame;
  207. bool hadFocus = view.HasFocus;
  208. bool couldFocus = view.CanFocus;
  209. if (hadFocus)
  210. {
  211. view.CanFocus = false; // If view had focus, this will ensure it doesn't and it stays that way
  212. }
  213. Debug.Assert (!view.HasFocus);
  214. InternalSubViews.Remove (view);
  215. // Clean up focus stuff
  216. _previouslyFocused = null;
  217. if (view.SuperView is { } && view.SuperView._previouslyFocused == this)
  218. {
  219. view.SuperView._previouslyFocused = null;
  220. }
  221. view.SuperView = null;
  222. SetNeedsLayout ();
  223. SetNeedsDraw ();
  224. foreach (View v in InternalSubViews)
  225. {
  226. if (v.Frame.IntersectsWith (touched))
  227. {
  228. view.SetNeedsDraw ();
  229. }
  230. }
  231. view.CanFocus = couldFocus; // Restore to previous value
  232. if (_previouslyFocused == view)
  233. {
  234. _previouslyFocused = null;
  235. }
  236. RaiseSubViewRemoved (view);
  237. return view;
  238. }
  239. internal void RaiseSubViewRemoved (View view)
  240. {
  241. OnSubViewRemoved (view);
  242. SubViewRemoved?.Invoke (this, new (this, view));
  243. }
  244. /// <summary>
  245. /// Called when a SubView has been removed from this View.
  246. /// </summary>
  247. /// <param name="view"></param>
  248. protected virtual void OnSubViewRemoved (View view) { }
  249. /// <summary>Raised when a SubView has been added to this View.</summary>
  250. public event EventHandler<SuperViewChangedEventArgs>? SubViewRemoved;
  251. /// <summary>
  252. /// Removes all SubViews added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.
  253. /// </summary>
  254. /// <remarks>
  255. /// <para>
  256. /// Normally SubViews will be disposed when this View is disposed. Removing a SubView causes ownership of the
  257. /// SubView's
  258. /// lifecycle to be transferred to the caller; the caller must call <see cref="Dispose()"/> on any Views that were
  259. /// added.
  260. /// </para>
  261. /// </remarks>
  262. /// <returns>
  263. /// A list of removed Views.
  264. /// </returns>
  265. public virtual IReadOnlyCollection<View> RemoveAll ()
  266. {
  267. List<View> removedList = new List<View> ();
  268. while (InternalSubViews.Count > 0)
  269. {
  270. View? removed = Remove (InternalSubViews [0]);
  271. if (removed is { })
  272. {
  273. removedList.Add (removed);
  274. }
  275. }
  276. return removedList.AsReadOnly ();
  277. }
  278. /// <summary>
  279. /// Removes all SubViews of a type added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.
  280. /// </summary>
  281. /// <remarks>
  282. /// <para>
  283. /// Normally SubViews will be disposed when this View is disposed. Removing a SubView causes ownership of the
  284. /// SubView's
  285. /// lifecycle to be transferred to the caller; the caller must call <see cref="Dispose()"/> on any Views that were
  286. /// added.
  287. /// </para>
  288. /// </remarks>
  289. /// <returns>
  290. /// A list of removed Views.
  291. /// </returns>
  292. public virtual IReadOnlyCollection<TView> RemoveAll<TView> () where TView : View
  293. {
  294. List<TView> removedList = new List<TView> ();
  295. foreach (TView view in InternalSubViews.OfType<TView> ().ToList ())
  296. {
  297. Remove (view);
  298. removedList.Add (view);
  299. }
  300. return removedList.AsReadOnly ();
  301. }
  302. #pragma warning disable CS0067 // The event is never used
  303. /// <summary>Raised when a SubView has been removed from this View.</summary>
  304. public event EventHandler<SuperViewChangedEventArgs>? Removed;
  305. #pragma warning restore CS0067 // The event is never used
  306. #endregion AddRemove
  307. // TODO: This drives a weird coupling of Application.Top and View. It's not clear why this is needed.
  308. /// <summary>Get the top superview of a given <see cref="View"/>.</summary>
  309. /// <returns>The superview view.</returns>
  310. internal View? GetTopSuperView (View? view = null, View? superview = null)
  311. {
  312. View? top = superview ?? Application.Top;
  313. for (View? v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView)
  314. {
  315. top = v;
  316. if (top == superview)
  317. {
  318. break;
  319. }
  320. }
  321. return top;
  322. }
  323. /// <summary>
  324. /// Gets whether <paramref name="view"/> is in the SubView hierarchy of <paramref name="start"/>.
  325. /// </summary>
  326. /// <param name="start">The View at the start of the hierarchy.</param>
  327. /// <param name="view">The View to test.</param>
  328. /// <param name="includeAdornments">Will search the subview hierarchy of the adornments if true.</param>
  329. /// <returns></returns>
  330. public static bool IsInHierarchy (View? start, View? view, bool includeAdornments = false)
  331. {
  332. if (view is null || start is null)
  333. {
  334. return false;
  335. }
  336. if (view == start)
  337. {
  338. return true;
  339. }
  340. foreach (View subView in start.InternalSubViews)
  341. {
  342. if (view == subView)
  343. {
  344. return true;
  345. }
  346. bool found = IsInHierarchy (subView, view, includeAdornments);
  347. if (found)
  348. {
  349. return found;
  350. }
  351. }
  352. if (includeAdornments)
  353. {
  354. bool found = IsInHierarchy (start.Padding, view, includeAdornments);
  355. if (found)
  356. {
  357. return found;
  358. }
  359. found = IsInHierarchy (start.Border, view, includeAdornments);
  360. if (found)
  361. {
  362. return found;
  363. }
  364. found = IsInHierarchy (start.Margin, view, includeAdornments);
  365. if (found)
  366. {
  367. return found;
  368. }
  369. }
  370. return false;
  371. }
  372. #region SubViewOrdering
  373. /// <summary>
  374. /// Moves <paramref name="subview"/> one position towards the end of the <see cref="SubViews"/> list.
  375. /// </summary>
  376. /// <param name="subview">The subview to move.</param>
  377. public void MoveSubViewTowardsEnd (View subview)
  378. {
  379. PerformActionForSubView (
  380. subview,
  381. x =>
  382. {
  383. int idx = InternalSubViews!.IndexOf (x);
  384. if (idx + 1 < InternalSubViews.Count)
  385. {
  386. InternalSubViews.Remove (x);
  387. InternalSubViews.Insert (idx + 1, x);
  388. }
  389. }
  390. );
  391. }
  392. /// <summary>
  393. /// Moves <paramref name="subview"/> to the end of the <see cref="SubViews"/> list.
  394. /// </summary>
  395. /// <param name="subview">The subview to move.</param>
  396. public void MoveSubViewToEnd (View subview)
  397. {
  398. PerformActionForSubView (
  399. subview,
  400. x =>
  401. {
  402. InternalSubViews!.Remove (x);
  403. InternalSubViews.Add (x);
  404. }
  405. );
  406. }
  407. /// <summary>
  408. /// Moves <paramref name="subview"/> one position towards the start of the <see cref="SubViews"/> list.
  409. /// </summary>
  410. /// <param name="subview">The subview to move.</param>
  411. public void MoveSubViewTowardsStart (View subview)
  412. {
  413. PerformActionForSubView (
  414. subview,
  415. x =>
  416. {
  417. int idx = InternalSubViews!.IndexOf (x);
  418. if (idx > 0)
  419. {
  420. InternalSubViews.Remove (x);
  421. InternalSubViews.Insert (idx - 1, x);
  422. }
  423. }
  424. );
  425. }
  426. /// <summary>
  427. /// Moves <paramref name="subview"/> to the start of the <see cref="SubViews"/> list.
  428. /// </summary>
  429. /// <param name="subview">The subview to move.</param>
  430. public void MoveSubViewToStart (View subview)
  431. {
  432. PerformActionForSubView (
  433. subview,
  434. x =>
  435. {
  436. InternalSubViews!.Remove (x);
  437. InternalSubViews.Insert (0, subview);
  438. }
  439. );
  440. }
  441. /// <summary>
  442. /// Internal API that runs <paramref name="action"/> on a subview if it is part of the <see cref="SubViews"/> list.
  443. /// </summary>
  444. /// <param name="subview"></param>
  445. /// <param name="action"></param>
  446. private void PerformActionForSubView (View subview, Action<View> action)
  447. {
  448. if (InternalSubViews.Contains (subview))
  449. {
  450. action (subview);
  451. }
  452. // BUGBUG: this is odd. Why is this needed?
  453. SetNeedsDraw ();
  454. subview.SetNeedsDraw ();
  455. }
  456. #endregion SubViewOrdering
  457. }