ApplicationPopover.cs 7.6 KB

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