MouseBindings.cs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. #nullable enable
  2. namespace Terminal.Gui;
  3. /// <summary>
  4. /// Provides a collection of <see cref="MouseBinding"/> objects bound to a combination of <see cref="MouseEventArgs"/>.
  5. /// </summary>
  6. /// <seealso cref="View.MouseBindings"/>
  7. /// <seealso cref="Command"/>
  8. public class MouseBindings
  9. {
  10. /// <summary>
  11. /// Initializes a new instance. This constructor is used when the <see cref="MouseBindings"/> are not bound to a
  12. /// <see cref="View"/>. This is used for Application.MouseBindings and unit tests.
  13. /// </summary>
  14. public MouseBindings () { }
  15. /// <summary>Adds a <see cref="MouseBinding"/> to the collection.</summary>
  16. /// <param name="mouseEvent"></param>
  17. /// <param name="binding"></param>
  18. public void Add (MouseEventArgs mouseEvent, MouseBinding binding)
  19. {
  20. if (TryGet (mouseEvent, out MouseBinding _))
  21. {
  22. throw new InvalidOperationException (@$"A binding for {mouseEvent} exists ({binding}).");
  23. }
  24. // IMPORTANT: Add a COPY of the key. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy
  25. // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus
  26. // IMPORTANT: Apply will update the Dictionary with the new key, but the old key will still be in the dictionary.
  27. // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details.
  28. Bindings.Add (mouseEvent, binding);
  29. }
  30. /// <summary>
  31. /// <para>Adds a new mouse flag combination that will trigger the commands in <paramref name="commands"/>.</para>
  32. /// <para>
  33. /// If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
  34. /// <paramref name="commands"/>.
  35. /// </para>
  36. /// </summary>
  37. /// <remarks>
  38. /// Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
  39. /// focus to another view and perform multiple commands there).
  40. /// </remarks>
  41. /// <param name="mouseEvents">The mouse flags to check.</param>
  42. /// <param name="commands">
  43. /// The command to invoked on the <see cref="View"/> when <paramref name="mouseEvents"/> is received. When
  44. /// multiple commands are provided,they will be applied in sequence. The bound <paramref name="mouseEvents"/> event will be
  45. /// consumed if any took effect.
  46. /// </param>
  47. public void Add (MouseEventArgs mouseEvents, params Command [] commands)
  48. {
  49. if (mouseEvents.Flags == MouseFlags.None)
  50. {
  51. throw new ArgumentException (@"Invalid MouseFlag", nameof (commands));
  52. }
  53. if (commands.Length == 0)
  54. {
  55. throw new ArgumentException (@"At least one command must be specified", nameof (commands));
  56. }
  57. if (TryGet (mouseEvents, out MouseBinding binding))
  58. {
  59. throw new InvalidOperationException (@$"A binding for {mouseEvents} exists ({binding}).");
  60. }
  61. Add (mouseEvents, new MouseBinding (commands));
  62. }
  63. // TODO: Add a dictionary comparer that ignores Scope
  64. // TODO: This should not be public!
  65. /// <summary>The collection of <see cref="MouseBinding"/> objects.</summary>
  66. public Dictionary<MouseEventArgs, MouseBinding> Bindings { get; } = new ();
  67. /// <summary>
  68. /// Gets the <see cref="MouseEventArgs"/> that are bound.
  69. /// </summary>
  70. /// <returns></returns>
  71. public IEnumerable<MouseEventArgs> GetBoundMouseEventArgs ()
  72. {
  73. return Bindings.Keys;
  74. }
  75. /// <summary>Removes all <see cref="MouseBinding"/> objects from the collection.</summary>
  76. public void Clear () { Bindings.Clear (); }
  77. /// <summary>
  78. /// Removes all bindings that trigger the given command set. Views can have multiple different events bound to
  79. /// the same command sets and this method will clear all of them.
  80. /// </summary>
  81. /// <param name="command"></param>
  82. public void Clear (params Command [] command)
  83. {
  84. KeyValuePair<MouseEventArgs, MouseBinding> [] kvps = Bindings
  85. .Where (kvp => kvp.Value.Commands.SequenceEqual (command))
  86. .ToArray ();
  87. foreach (KeyValuePair<MouseEventArgs, MouseBinding> kvp in kvps)
  88. {
  89. Remove (kvp.Key);
  90. }
  91. }
  92. /// <summary>Gets the <see cref="MouseBinding"/> for the specified combination of <see cref="MouseEventArgs"/>.</summary>
  93. /// <param name="mouseEvents"></param>
  94. /// <returns></returns>
  95. public MouseBinding Get (MouseEventArgs mouseEvents)
  96. {
  97. if (TryGet (mouseEvents, out MouseBinding binding))
  98. {
  99. return binding;
  100. }
  101. throw new InvalidOperationException ($"{mouseEvents} is not bound.");
  102. }
  103. /// <summary>Gets the array of <see cref="Command"/>s bound to <paramref name="mouseEvents"/> if it exists.</summary>
  104. /// <param name="mouseEvents">The key to check.</param>
  105. /// <returns>
  106. /// The array of <see cref="Command"/>s if <paramref name="mouseEvents"/> is bound. An empty <see cref="Command"/> array
  107. /// if not.
  108. /// </returns>
  109. public Command [] GetCommands (MouseEventArgs mouseEvents)
  110. {
  111. if (TryGet (mouseEvents, out MouseBinding bindings))
  112. {
  113. return bindings.Commands;
  114. }
  115. return [];
  116. }
  117. /// <summary>Gets the first combination of <see cref="MouseEventArgs"/> bound to the set of commands specified by <paramref name="commands"/>.</summary>
  118. /// <param name="commands">The set of commands to search.</param>
  119. /// <returns>The first combination of <see cref="MouseEventArgs"/> bound to the set of commands specified by <paramref name="commands"/>. <see langword="null"/> if the set of caommands was not found.</returns>
  120. public MouseEventArgs? GetMouseEventArgsFromCommands (params Command [] commands)
  121. {
  122. return Bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key;
  123. }
  124. /// <summary>Gets combination of <see cref="MouseEventArgs"/> bound to the set of commands specified by <paramref name="commands"/>.</summary>
  125. /// <param name="commands">The set of commands to search.</param>
  126. /// <returns>The combination of <see cref="MouseEventArgs"/> bound to the set of commands specified by <paramref name="commands"/>. An empty list if the set of caommands was not found.</returns>
  127. public IEnumerable<MouseEventArgs> GetAllMouseEventArgsFromCommands (params Command [] commands)
  128. {
  129. return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key);
  130. }
  131. /// <summary>Removes a <see cref="MouseBinding"/> from the collection.</summary>
  132. /// <param name="mouseEvents"></param>
  133. public void Remove (MouseEventArgs mouseEvents)
  134. {
  135. if (!TryGet (mouseEvents, out MouseBinding _))
  136. {
  137. return;
  138. }
  139. Bindings.Remove (mouseEvents);
  140. }
  141. /// <summary>Replaces the commands already bound to a combination of <see cref="MouseEventArgs"/>.</summary>
  142. /// <remarks>
  143. /// <para>
  144. /// If the combination of <see cref="MouseEventArgs"/> is not already bound, it will be added.
  145. /// </para>
  146. /// </remarks>
  147. /// <param name="mouseEvents">The combination of <see cref="MouseEventArgs"/> bound to the command to be replaced.</param>
  148. /// <param name="commands">The set of commands to replace the old ones with.</param>
  149. public void ReplaceCommands (MouseEventArgs mouseEvents, params Command [] commands)
  150. {
  151. if (TryGet (mouseEvents, out MouseBinding binding))
  152. {
  153. binding.Commands = commands;
  154. }
  155. else
  156. {
  157. Add (mouseEvents, commands);
  158. }
  159. }
  160. /// <summary>Replaces a <see cref="MouseEventArgs"/> combination already bound to a set of <see cref="Command"/>s.</summary>
  161. /// <remarks></remarks>
  162. /// <param name="oldMouseEventArgs">The <see cref="MouseEventArgs"/> to be replaced.</param>
  163. /// <param name="newMouseEventArgs">The new <see cref="MouseEventArgs"/> to be used. If <see cref="Key.Empty"/> no action will be taken.</param>
  164. public void ReplaceKey (MouseEventArgs oldMouseEventArgs, MouseEventArgs newMouseEventArgs)
  165. {
  166. if (!TryGet (oldMouseEventArgs, out MouseBinding _))
  167. {
  168. throw new InvalidOperationException ($"Key {oldMouseEventArgs} is not bound.");
  169. }
  170. MouseBinding value = Bindings [oldMouseEventArgs];
  171. Remove (oldMouseEventArgs);
  172. Add (newMouseEventArgs, value);
  173. }
  174. /// <summary>Gets the commands bound with the specified <see cref="MouseEventArgs"/>.</summary>
  175. /// <remarks></remarks>
  176. /// <param name="mouseEvents">The key to check.</param>
  177. /// <param name="binding">
  178. /// When this method returns, contains the commands bound with the specified mouse flags, if the mouse flags are
  179. /// found; otherwise, null. This parameter is passed uninitialized.
  180. /// </param>
  181. /// <returns><see langword="true"/> if the mouse flags are bound; otherwise <see langword="false"/>.</returns>
  182. public bool TryGet (MouseEventArgs mouseEvents, out MouseBinding binding)
  183. {
  184. binding = new ([]);
  185. return Bindings.TryGetValue (mouseEvents, out binding);
  186. }
  187. }