ApplicationPopover.cs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. using System.Diagnostics;
  2. namespace Terminal.Gui.App;
  3. /// <summary>
  4. /// Helper class for support of <see cref="IPopover"/> views for <see cref="IApplication"/>. Held by
  5. /// <see cref="IApplication.Popover"/>
  6. /// </summary>
  7. public sealed class ApplicationPopover : IDisposable
  8. {
  9. /// <summary>
  10. /// Initializes a new instance of the <see cref="ApplicationPopover"/> class.
  11. /// </summary>
  12. public ApplicationPopover () { }
  13. /// <summary>
  14. /// The <see cref="IApplication"/> instance used by this instance.
  15. /// </summary>
  16. public IApplication? App { get; set; }
  17. private readonly List<IPopover> _popovers = [];
  18. /// <summary>
  19. /// Gets the list of popovers registered with the application.
  20. /// </summary>
  21. public IReadOnlyCollection<IPopover> Popovers => _popovers.AsReadOnly ();
  22. /// <summary>
  23. /// Registers <paramref name="popover"/> with the application.
  24. /// This enables the popover to receive keyboard events even when it is not active.
  25. /// </summary>
  26. /// <remarks>
  27. /// When a popover is registered, the View instance lifetime is managed by the application. Call
  28. /// <see cref="DeRegister"/>
  29. /// to manage the lifetime of the popover directly.
  30. /// </remarks>
  31. /// <param name="popover"></param>
  32. /// <returns><paramref name="popover"/>, after it has been registered.</returns>
  33. public IPopover? Register (IPopover? popover)
  34. {
  35. if (popover is { } && !IsRegistered (popover))
  36. {
  37. // When created, set IPopover.Toplevel to the current Application.Current
  38. popover.Current ??= App?.Current;
  39. if (popover is View popoverView)
  40. {
  41. popoverView.App = App;
  42. }
  43. _popovers.Add (popover);
  44. }
  45. return popover;
  46. }
  47. /// <summary>
  48. /// Indicates whether a popover has been registered or not.
  49. /// </summary>
  50. /// <param name="popover"></param>
  51. /// <returns></returns>
  52. public bool IsRegistered (IPopover? popover) => popover is { } && _popovers.Contains (popover);
  53. /// <summary>
  54. /// De-registers <paramref name="popover"/> with the application. Use this to remove the popover and it's
  55. /// keyboard bindings from the application.
  56. /// </summary>
  57. /// <remarks>
  58. /// When a popover is registered, the View instance lifetime is managed by the application. Call
  59. /// <see cref="DeRegister"/>
  60. /// to manage the lifetime of the popover directly.
  61. /// </remarks>
  62. /// <param name="popover"></param>
  63. /// <returns></returns>
  64. public bool DeRegister (IPopover? popover)
  65. {
  66. if (popover is null || !IsRegistered (popover))
  67. {
  68. return false;
  69. }
  70. if (GetActivePopover () == popover)
  71. {
  72. _activePopover = null;
  73. }
  74. _popovers.Remove (popover);
  75. return true;
  76. }
  77. private IPopover? _activePopover;
  78. /// <summary>
  79. /// Gets the active popover, if any.
  80. /// </summary>
  81. /// <remarks>
  82. /// Note, the active pop over does not necessarily to be registered with the application.
  83. /// </remarks>
  84. /// <returns></returns>
  85. public IPopover? GetActivePopover () { return _activePopover; }
  86. /// <summary>
  87. /// Shows <paramref name="popover"/>. IPopover implementations should use OnVisibleChanaged/VisibleChanged to be
  88. /// notified when the user has done something to cause the popover to be hidden.
  89. /// </summary>
  90. /// <remarks>
  91. /// <para>
  92. /// This API calls <see cref="Register"/>. To disable the popover from processing keyboard events,
  93. /// either call <see cref="DeRegister"/> to
  94. /// remove the popover from the application or set <see cref="View.Enabled"/> to <see langword="false"/>.
  95. /// </para>
  96. /// </remarks>
  97. /// <param name="popover"></param>
  98. public void Show (IPopover? popover)
  99. {
  100. if (!IsRegistered (popover))
  101. {
  102. throw new InvalidOperationException (@"Popovers must be registered before being shown.");
  103. }
  104. // If there's an existing popover, hide it.
  105. if (_activePopover is View popoverView)
  106. {
  107. popoverView.App = App;
  108. popoverView.Visible = false;
  109. _activePopover = null;
  110. }
  111. if (popover is View newPopover)
  112. {
  113. if (!(newPopover.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent) &&
  114. newPopover.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse)))
  115. {
  116. throw new InvalidOperationException ("Popovers must have ViewportSettings.Transparent and ViewportSettings.TransparentMouse set.");
  117. }
  118. if (newPopover.KeyBindings.GetFirstFromCommands (Command.Quit) is null)
  119. {
  120. throw new InvalidOperationException ("Popovers must have a key binding for Command.Quit.");
  121. }
  122. if (!newPopover.IsInitialized)
  123. {
  124. newPopover.BeginInit ();
  125. newPopover.EndInit ();
  126. }
  127. _activePopover = newPopover as IPopover;
  128. newPopover.Enabled = true;
  129. newPopover.Visible = true;
  130. }
  131. }
  132. /// <summary>
  133. /// Causes the specified popover to be hidden.
  134. /// If the popover is derived from <see cref="PopoverBaseImpl"/>, this is the same as setting
  135. /// <see cref="View.Visible"/> to <see langword="false"/>.
  136. /// </summary>
  137. /// <param name="popover"></param>
  138. public void Hide (IPopover? popover)
  139. {
  140. // If there's an existing popover, hide it.
  141. if (_activePopover is View popoverView && popoverView == popover)
  142. {
  143. _activePopover = null;
  144. popoverView.Visible = false;
  145. popoverView.App?.Current?.SetNeedsDraw ();
  146. }
  147. }
  148. /// <summary>
  149. /// Hides a popover view if it supports the quit command and is currently visible. It checks for the command's
  150. /// support before hiding.
  151. /// </summary>
  152. /// <param name="visiblePopover">The view that is being checked and potentially hidden based on its visibility and command support.</param>
  153. internal static void HideWithQuitCommand (View visiblePopover)
  154. {
  155. if (visiblePopover.Visible
  156. && (!visiblePopover.GetSupportedCommands ().Contains (Command.Quit)
  157. || (visiblePopover.InvokeCommand (Command.Quit) is true && visiblePopover.Visible)))
  158. {
  159. visiblePopover.Visible = false;
  160. }
  161. }
  162. /// <summary>
  163. /// Called when the user presses a key. Dispatches the key to the active popover, if any,
  164. /// otherwise to the popovers in the order they were registered. Inactive popovers only get hotkeys.
  165. /// </summary>
  166. /// <param name="key"></param>
  167. /// <returns></returns>
  168. internal bool DispatchKeyDown (Key key)
  169. {
  170. // Do active first - Active gets all key down events.
  171. View? activePopover = GetActivePopover () as View;
  172. if (activePopover is { Visible: true })
  173. {
  174. //Logging.Debug ($"Active - Calling NewKeyDownEvent ({key}) on {activePopover.Title}");
  175. if (activePopover.NewKeyDownEvent (key))
  176. {
  177. return true;
  178. }
  179. }
  180. // If the active popover didn't handle the key, try the inactive ones.
  181. // Inactive only get hotkeys
  182. bool? hotKeyHandled = null;
  183. foreach (IPopover popover in _popovers)
  184. {
  185. if (popover == activePopover
  186. || popover is not View popoverView
  187. || (popover.Current is { } && popover.Current != App?.Current))
  188. {
  189. continue;
  190. }
  191. // hotKeyHandled = popoverView.InvokeCommandsBoundToHotKey (key);
  192. //Logging.Debug ($"Inactive - Calling NewKeyDownEvent ({key}) on {popoverView.Title}");
  193. popoverView.App ??= App;
  194. hotKeyHandled = popoverView.NewKeyDownEvent (key);
  195. if (hotKeyHandled is true)
  196. {
  197. return true;
  198. }
  199. }
  200. return hotKeyHandled is true;
  201. }
  202. /// <inheritdoc/>
  203. public void Dispose ()
  204. {
  205. foreach (IPopover popover in _popovers)
  206. {
  207. if (popover is View view)
  208. {
  209. view.Dispose ();
  210. }
  211. }
  212. _popovers.Clear ();
  213. }
  214. }