2
0

RadioGroup.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. using System.Text;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. namespace Terminal.Gui;
  6. /// <summary>
  7. /// Displays a group of labels each with a selected indicator. Only one of those can be selected at a given time.
  8. /// </summary>
  9. public class RadioGroup : View {
  10. int _selected = -1;
  11. int _cursor;
  12. DisplayModeLayout _displayMode;
  13. int _horizontalSpace = 2;
  14. List<(int pos, int length)> _horizontal;
  15. /// <summary>
  16. /// Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Computed"/> layout.
  17. /// </summary>
  18. public RadioGroup () : this (radioLabels: new string [] { }) { }
  19. /// <summary>
  20. /// Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Computed"/> layout.
  21. /// </summary>
  22. /// <param name="radioLabels">The radio labels; an array of strings that can contain hotkeys using an underscore before the letter.</param>
  23. /// <param name="selected">The index of the item to be selected, the value is clamped to the number of items.</param>
  24. public RadioGroup (string [] radioLabels, int selected = 0) : base ()
  25. {
  26. SetInitialProperties (Rect.Empty, radioLabels, selected);
  27. }
  28. /// <summary>
  29. /// Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Absolute"/> layout.
  30. /// </summary>
  31. /// <param name="rect">Boundaries for the radio group.</param>
  32. /// <param name="radioLabels">The radio labels; an array of strings that can contain hotkeys using an underscore before the letter.</param>
  33. /// <param name="selected">The index of item to be selected, the value is clamped to the number of items.</param>
  34. public RadioGroup (Rect rect, string [] radioLabels, int selected = 0) : base (rect)
  35. {
  36. SetInitialProperties (rect, radioLabels, selected);
  37. }
  38. /// <summary>
  39. /// Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Absolute"/> layout.
  40. /// The <see cref="View"/> frame is computed from the provided radio labels.
  41. /// </summary>
  42. /// <param name="x">The x coordinate.</param>
  43. /// <param name="y">The y coordinate.</param>
  44. /// <param name="radioLabels">The radio labels; an array of strings that can contain hotkeys using an underscore before the letter.</param>
  45. /// <param name="selected">The item to be selected, the value is clamped to the number of items.</param>
  46. public RadioGroup (int x, int y, string [] radioLabels, int selected = 0) :
  47. this (MakeRect (x, y, radioLabels != null ? radioLabels.ToList () : null), radioLabels, selected)
  48. { }
  49. void SetInitialProperties (Rect rect, string [] radioLabels, int selected)
  50. {
  51. HotKeySpecifier = new Rune ('_');
  52. if (radioLabels != null) {
  53. RadioLabels = radioLabels;
  54. }
  55. _selected = selected;
  56. Frame = rect;
  57. CanFocus = true;
  58. // Things this view knows how to do
  59. AddCommand (Command.LineUp, () => { MoveUp (); return true; });
  60. AddCommand (Command.LineDown, () => { MoveDown (); return true; });
  61. AddCommand (Command.TopHome, () => { MoveHome (); return true; });
  62. AddCommand (Command.BottomEnd, () => { MoveEnd (); return true; });
  63. AddCommand (Command.Accept, () => { SelectItem (); return true; });
  64. // Default keybindings for this view
  65. KeyBindings.Add (KeyCode.CursorUp, Command.LineUp);
  66. KeyBindings.Add (KeyCode.CursorDown, Command.LineDown);
  67. KeyBindings.Add (KeyCode.Home, Command.TopHome);
  68. KeyBindings.Add (KeyCode.End, Command.BottomEnd);
  69. KeyBindings.Add (KeyCode.Space, Command.Accept);
  70. LayoutStarted += RadioGroup_LayoutStarted;
  71. }
  72. void RadioGroup_LayoutStarted (object sender, EventArgs e)
  73. {
  74. SetWidthHeight (_radioLabels);
  75. }
  76. /// <summary>
  77. /// Gets or sets the <see cref="DisplayModeLayout"/> for this <see cref="RadioGroup"/>.
  78. /// </summary>
  79. public DisplayModeLayout DisplayMode {
  80. get { return _displayMode; }
  81. set {
  82. if (_displayMode != value) {
  83. _displayMode = value;
  84. SetWidthHeight (_radioLabels);
  85. SetNeedsDisplay ();
  86. }
  87. }
  88. }
  89. /// <summary>
  90. /// Gets or sets the horizontal space for this <see cref="RadioGroup"/> if the <see cref="DisplayMode"/> is <see cref="DisplayModeLayout.Horizontal"/>
  91. /// </summary>
  92. public int HorizontalSpace {
  93. get { return _horizontalSpace; }
  94. set {
  95. if (_horizontalSpace != value && _displayMode == DisplayModeLayout.Horizontal) {
  96. _horizontalSpace = value;
  97. SetWidthHeight (_radioLabels);
  98. UpdateTextFormatterText ();
  99. SetNeedsDisplay ();
  100. }
  101. }
  102. }
  103. void SetWidthHeight (List<string> radioLabels)
  104. {
  105. switch (_displayMode) {
  106. case DisplayModeLayout.Vertical:
  107. var r = MakeRect (0, 0, radioLabels);
  108. Bounds = new Rect (Bounds.Location, new Size (r.Width, radioLabels.Count));
  109. break;
  110. case DisplayModeLayout.Horizontal:
  111. CalculateHorizontalPositions ();
  112. var length = 0;
  113. foreach (var item in _horizontal) {
  114. length += item.length;
  115. }
  116. var hr = new Rect (0, 0, length, 1);
  117. if (IsAdded && LayoutStyle == LayoutStyle.Computed) {
  118. Width = hr.Width;
  119. Height = 1;
  120. } else {
  121. Bounds = new Rect (Bounds.Location, new Size (hr.Width, radioLabels.Count));
  122. }
  123. break;
  124. }
  125. }
  126. static Rect MakeRect (int x, int y, List<string> radioLabels)
  127. {
  128. if (radioLabels == null) {
  129. return new Rect (x, y, 0, 0);
  130. }
  131. int width = 0;
  132. foreach (var s in radioLabels) {
  133. width = Math.Max (s.GetColumns () + 2, width);
  134. }
  135. return new Rect (x, y, width, radioLabels.Count);
  136. }
  137. List<string> _radioLabels = new List<string> ();
  138. /// <summary>
  139. /// The radio labels to display. A key binding will be added for each radio radio enabling the user
  140. /// to select and/or focus the radio label using the keyboard. See <see cref="View.HotKey"/> for details
  141. /// on how HotKeys work.
  142. /// </summary>
  143. /// <value>The radio labels.</value>
  144. public string [] RadioLabels {
  145. get => _radioLabels.ToArray ();
  146. set {
  147. // Remove old hot key bindings
  148. foreach (var label in _radioLabels) {
  149. if (TextFormatter.FindHotKey (label, HotKeySpecifier, true, out _, out var hotKey)) {
  150. AddKeyBindingsForHotKey (hotKey, KeyCode.Null);
  151. }
  152. }
  153. var prevCount = _radioLabels.Count;
  154. _radioLabels = value.ToList ();
  155. foreach (var label in _radioLabels) {
  156. if (TextFormatter.FindHotKey (label, HotKeySpecifier, true, out _, out var hotKey)) {
  157. AddKeyBindingsForHotKey (KeyCode.Null, hotKey);
  158. }
  159. }
  160. if (IsInitialized && prevCount != _radioLabels.Count) {
  161. SetWidthHeight (_radioLabels);
  162. }
  163. SelectedItem = 0;
  164. _cursor = 0;
  165. SetNeedsDisplay ();
  166. }
  167. }
  168. /// <inheritdoc/>
  169. public override bool? OnInvokingKeyBindings (Key keyEvent)
  170. {
  171. // This is a bit of a hack. We want to handle the key bindings for the radio group but
  172. // InvokeKeyBindings doesn't pass any context so we can't tell if the key binding is for
  173. // the radio group or for one of the radio buttons. So before we call the base class
  174. // we set SelectedItem appropriately.
  175. var key = keyEvent;
  176. if (KeyBindings.TryGet (key, out _)) {
  177. // Search RadioLabels
  178. for (int i = 0; i < _radioLabels.Count; i++) {
  179. if (TextFormatter.FindHotKey (_radioLabels [i], HotKeySpecifier, true, out _, out var hotKey)
  180. && (key.NoAlt.NoCtrl.NoShift) == hotKey) {
  181. SelectedItem = i;
  182. keyEvent.Scope = KeyBindingScope.HotKey;
  183. break;
  184. }
  185. }
  186. }
  187. return base.OnInvokingKeyBindings (keyEvent);
  188. }
  189. void CalculateHorizontalPositions ()
  190. {
  191. if (_displayMode == DisplayModeLayout.Horizontal) {
  192. _horizontal = new List<(int pos, int length)> ();
  193. int start = 0;
  194. int length = 0;
  195. for (int i = 0; i < _radioLabels.Count; i++) {
  196. start += length;
  197. length = _radioLabels [i].GetColumns () + 2 + (i < _radioLabels.Count - 1 ? _horizontalSpace : 0);
  198. _horizontal.Add ((start, length));
  199. }
  200. }
  201. }
  202. ///<inheritdoc/>
  203. public override void OnDrawContent (Rect contentArea)
  204. {
  205. base.OnDrawContent (contentArea);
  206. Driver.SetAttribute (GetNormalColor ());
  207. for (int i = 0; i < _radioLabels.Count; i++) {
  208. switch (DisplayMode) {
  209. case DisplayModeLayout.Vertical:
  210. Move (0, i);
  211. break;
  212. case DisplayModeLayout.Horizontal:
  213. Move (_horizontal [i].pos, 0);
  214. break;
  215. }
  216. var rl = _radioLabels [i];
  217. Driver.SetAttribute (GetNormalColor ());
  218. Driver.AddStr ($"{(i == _selected ? CM.Glyphs.Selected : CM.Glyphs.UnSelected)} ");
  219. TextFormatter.FindHotKey (rl, HotKeySpecifier, true, out int hotPos, out var hotKey);
  220. if (hotPos != -1 && (hotKey != KeyCode.Null)) {
  221. var rlRunes = rl.ToRunes ();
  222. for (int j = 0; j < rlRunes.Length; j++) {
  223. Rune rune = rlRunes [j];
  224. if (j == hotPos && i == _cursor) {
  225. Application.Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : GetHotNormalColor ());
  226. } else if (j == hotPos && i != _cursor) {
  227. Application.Driver.SetAttribute (GetHotNormalColor ());
  228. } else if (HasFocus && i == _cursor) {
  229. Application.Driver.SetAttribute (ColorScheme.Focus);
  230. }
  231. if (rune == HotKeySpecifier && j + 1 < rlRunes.Length) {
  232. j++;
  233. rune = rlRunes [j];
  234. if (i == _cursor) {
  235. Application.Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : GetHotNormalColor ());
  236. } else if (i != _cursor) {
  237. Application.Driver.SetAttribute (GetHotNormalColor ());
  238. }
  239. }
  240. Application.Driver.AddRune (rune);
  241. Driver.SetAttribute (GetNormalColor ());
  242. }
  243. } else {
  244. DrawHotString (rl, HasFocus && i == _cursor, ColorScheme);
  245. }
  246. }
  247. }
  248. ///<inheritdoc/>
  249. public override void PositionCursor ()
  250. {
  251. switch (DisplayMode) {
  252. case DisplayModeLayout.Vertical:
  253. Move (0, _cursor);
  254. break;
  255. case DisplayModeLayout.Horizontal:
  256. Move (_horizontal [_cursor].pos, 0);
  257. break;
  258. }
  259. }
  260. /// <summary>
  261. /// Invoked when the selected radio label has changed.
  262. /// </summary>
  263. public event EventHandler<SelectedItemChangedArgs> SelectedItemChanged;
  264. /// <summary>
  265. /// The currently selected item from the list of radio labels
  266. /// </summary>
  267. /// <value>The selected.</value>
  268. public int SelectedItem {
  269. get => _selected;
  270. set {
  271. OnSelectedItemChanged (value, SelectedItem);
  272. _cursor = _selected;
  273. SetNeedsDisplay ();
  274. }
  275. }
  276. /// <summary>
  277. /// Allow to invoke the <see cref="SelectedItemChanged"/> after their creation.
  278. /// </summary>
  279. public void Refresh ()
  280. {
  281. OnSelectedItemChanged (_selected, -1);
  282. }
  283. /// <summary>
  284. /// Called whenever the current selected item changes. Invokes the <see cref="SelectedItemChanged"/> event.
  285. /// </summary>
  286. /// <param name="selectedItem"></param>
  287. /// <param name="previousSelectedItem"></param>
  288. public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
  289. {
  290. _selected = selectedItem;
  291. SelectedItemChanged?.Invoke (this, new SelectedItemChangedArgs (selectedItem, previousSelectedItem));
  292. }
  293. void SelectItem ()
  294. {
  295. SelectedItem = _cursor;
  296. }
  297. void MoveEnd ()
  298. {
  299. _cursor = Math.Max (_radioLabels.Count - 1, 0);
  300. }
  301. void MoveHome ()
  302. {
  303. _cursor = 0;
  304. }
  305. void MoveDown ()
  306. {
  307. if (_cursor + 1 < _radioLabels.Count) {
  308. _cursor++;
  309. SetNeedsDisplay ();
  310. } else if (_cursor > 0) {
  311. _cursor = 0;
  312. SetNeedsDisplay ();
  313. }
  314. }
  315. void MoveUp ()
  316. {
  317. if (_cursor > 0) {
  318. _cursor--;
  319. SetNeedsDisplay ();
  320. } else if (_radioLabels.Count - 1 > 0) {
  321. _cursor = _radioLabels.Count - 1;
  322. SetNeedsDisplay ();
  323. }
  324. }
  325. ///<inheritdoc/>
  326. public override bool MouseEvent (MouseEvent me)
  327. {
  328. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)) {
  329. return false;
  330. }
  331. if (!CanFocus) {
  332. return false;
  333. }
  334. SetFocus ();
  335. int boundsX = me.X;
  336. int boundsY = me.Y;
  337. var pos = _displayMode == DisplayModeLayout.Horizontal ? boundsX : boundsY;
  338. var rCount = _displayMode == DisplayModeLayout.Horizontal ? _horizontal.Last ().pos + _horizontal.Last ().length : _radioLabels.Count;
  339. if (pos < rCount) {
  340. var c = _displayMode == DisplayModeLayout.Horizontal ? _horizontal.FindIndex ((x) => x.pos <= boundsX && x.pos + x.length - 2 >= boundsX) : boundsY;
  341. if (c > -1) {
  342. _cursor = SelectedItem = c;
  343. SetNeedsDisplay ();
  344. }
  345. }
  346. return true;
  347. }
  348. ///<inheritdoc/>
  349. public override bool OnEnter (View view)
  350. {
  351. Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
  352. return base.OnEnter (view);
  353. }
  354. }
  355. /// <summary>
  356. /// Used for choose the display mode of this <see cref="RadioGroup"/>
  357. /// </summary>
  358. public enum DisplayModeLayout {
  359. /// <summary>
  360. /// Vertical mode display. It's the default.
  361. /// </summary>
  362. Vertical,
  363. /// <summary>
  364. /// Horizontal mode display.
  365. /// </summary>
  366. Horizontal
  367. }