RadioGroup.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. using NStack;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. namespace Terminal.Gui {
  6. /// <summary>
  7. /// <see cref="RadioGroup"/> shows a group of radio labels, 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. List<(int pos, int length)> horizontal;
  14. void Init (Rect rect, ustring [] radioLabels, int selected)
  15. {
  16. if (radioLabels == null) {
  17. this.radioLabels = new List<ustring>();
  18. } else {
  19. this.radioLabels = radioLabels.ToList ();
  20. }
  21. this.selected = selected;
  22. SetWidthHeight (this.radioLabels);
  23. CanFocus = true;
  24. }
  25. /// <summary>
  26. /// Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Computed"/> layout.
  27. /// </summary>
  28. public RadioGroup () : this (radioLabels: new ustring [] { }) { }
  29. /// <summary>
  30. /// Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Computed"/> layout.
  31. /// </summary>
  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 the item to be selected, the value is clamped to the number of items.</param>
  34. public RadioGroup (ustring [] radioLabels, int selected = 0) : base ()
  35. {
  36. Init (Rect.Empty, radioLabels, selected);
  37. }
  38. /// <summary>
  39. /// Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Absolute"/> layout.
  40. /// </summary>
  41. /// <param name="rect">Boundaries for the radio group.</param>
  42. /// <param name="radioLabels">The radio labels; an array of strings that can contain hotkeys using an underscore before the letter.</param>
  43. /// <param name="selected">The index of item to be selected, the value is clamped to the number of items.</param>
  44. public RadioGroup (Rect rect, ustring [] radioLabels, int selected = 0) : base (rect)
  45. {
  46. Init (rect, radioLabels, selected);
  47. }
  48. /// <summary>
  49. /// Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Absolute"/> layout.
  50. /// The <see cref="View"/> frame is computed from the provided radio labels.
  51. /// </summary>
  52. /// <param name="x">The x coordinate.</param>
  53. /// <param name="y">The y coordinate.</param>
  54. /// <param name="radioLabels">The radio labels; an array of strings that can contain hotkeys using an underscore before the letter.</param>
  55. /// <param name="selected">The item to be selected, the value is clamped to the number of items.</param>
  56. public RadioGroup (int x, int y, ustring [] radioLabels, int selected = 0) :
  57. this (MakeRect (x, y, radioLabels != null ? radioLabels.ToList() : null), radioLabels, selected) { }
  58. /// <summary>
  59. /// Gets or sets the <see cref="DisplayModeLayout"/> for this <see cref="RadioGroup"/>.
  60. /// </summary>
  61. public DisplayModeLayout DisplayMode {
  62. get { return displayMode; }
  63. set {
  64. if (displayMode != value) {
  65. displayMode = value;
  66. SetWidthHeight (radioLabels);
  67. SetNeedsDisplay ();
  68. }
  69. }
  70. }
  71. void SetWidthHeight (List<ustring> radioLabels)
  72. {
  73. switch (displayMode) {
  74. case DisplayModeLayout.Vertical:
  75. var r = MakeRect (0, 0, radioLabels);
  76. if (LayoutStyle == LayoutStyle.Computed) {
  77. Width = r.Width;
  78. Height = radioLabels.Count;
  79. } else {
  80. Frame = new Rect (Frame.Location, new Size (r.Width, radioLabels.Count));
  81. }
  82. break;
  83. case DisplayModeLayout.Horizontal:
  84. CalculateHorizontalPositions ();
  85. var length = 0;
  86. foreach (var item in horizontal) {
  87. length += item.length;
  88. }
  89. var hr = new Rect (0, 0, length, 1);
  90. if (LayoutStyle == LayoutStyle.Computed) {
  91. Width = hr.Width;
  92. Height = 1;
  93. }
  94. break;
  95. }
  96. }
  97. static Rect MakeRect (int x, int y, List<ustring> radioLabels)
  98. {
  99. int width = 0;
  100. if (radioLabels == null) {
  101. return new Rect (x, y, width, 0);
  102. }
  103. foreach (var s in radioLabels)
  104. width = Math.Max (s.RuneCount + 3, width);
  105. return new Rect (x, y, width, radioLabels.Count);
  106. }
  107. List<ustring> radioLabels = new List<ustring> ();
  108. /// <summary>
  109. /// The radio labels to display
  110. /// </summary>
  111. /// <value>The radio labels.</value>
  112. public ustring [] RadioLabels {
  113. get => radioLabels.ToArray();
  114. set {
  115. var prevCount = radioLabels.Count;
  116. radioLabels = value.ToList ();
  117. if (prevCount != radioLabels.Count) {
  118. SetWidthHeight (radioLabels);
  119. }
  120. SelectedItem = 0;
  121. cursor = 0;
  122. SetNeedsDisplay ();
  123. }
  124. }
  125. private void CalculateHorizontalPositions ()
  126. {
  127. if (displayMode == DisplayModeLayout.Horizontal) {
  128. horizontal = new List<(int pos, int length)> ();
  129. int start = 0;
  130. int length = 0;
  131. for (int i = 0; i < radioLabels.Count; i++) {
  132. start += length;
  133. length = radioLabels [i].RuneCount + 2;
  134. horizontal.Add ((start, length));
  135. }
  136. }
  137. }
  138. //// Redraws the RadioGroup
  139. //void Update(List<ustring> newRadioLabels)
  140. //{
  141. // for (int i = 0; i < radioLabels.Count; i++) {
  142. // Move(0, i);
  143. // Driver.SetAttribute(ColorScheme.Normal);
  144. // Driver.AddStr(ustring.Make(new string (' ', radioLabels[i].RuneCount + 4)));
  145. // }
  146. // if (newRadioLabels.Count != radioLabels.Count) {
  147. // SetWidthHeight(newRadioLabels);
  148. // }
  149. //}
  150. ///<inheritdoc/>
  151. public override void Redraw (Rect bounds)
  152. {
  153. Driver.SetAttribute (ColorScheme.Normal);
  154. Clear ();
  155. for (int i = 0; i < radioLabels.Count; i++) {
  156. switch (DisplayMode) {
  157. case DisplayModeLayout.Vertical:
  158. Move (0, i);
  159. break;
  160. case DisplayModeLayout.Horizontal:
  161. Move (horizontal [i].pos, 0);
  162. break;
  163. }
  164. Driver.SetAttribute (ColorScheme.Normal);
  165. Driver.AddStr (ustring.Make(new Rune[] { (i == selected ? Driver.Selected : Driver.UnSelected), ' '}));
  166. DrawHotString (radioLabels [i], HasFocus && i == cursor, ColorScheme);
  167. }
  168. }
  169. ///<inheritdoc/>
  170. public override void PositionCursor ()
  171. {
  172. switch (DisplayMode) {
  173. case DisplayModeLayout.Vertical:
  174. Move (0, cursor);
  175. break;
  176. case DisplayModeLayout.Horizontal:
  177. Move (horizontal [cursor].pos, 0);
  178. break;
  179. }
  180. }
  181. // TODO: Make this a global class
  182. /// <summary>
  183. /// Event arguments for the SelectedItemChagned event.
  184. /// </summary>
  185. public class SelectedItemChangedArgs : EventArgs {
  186. /// <summary>
  187. /// Gets the index of the item that was previously selected. -1 if there was no previous selection.
  188. /// </summary>
  189. public int PreviousSelectedItem { get; }
  190. /// <summary>
  191. /// Gets the index of the item that is now selected. -1 if there is no selection.
  192. /// </summary>
  193. public int SelectedItem { get; }
  194. /// <summary>
  195. /// Initializes a new <see cref="SelectedItemChangedArgs"/> class.
  196. /// </summary>
  197. /// <param name="selectedItem"></param>
  198. /// <param name="previousSelectedItem"></param>
  199. public SelectedItemChangedArgs(int selectedItem, int previousSelectedItem)
  200. {
  201. PreviousSelectedItem = previousSelectedItem;
  202. SelectedItem = selectedItem;
  203. }
  204. }
  205. /// <summary>
  206. /// Invoked when the selected radio label has changed.
  207. /// </summary>
  208. public Action<SelectedItemChangedArgs> SelectedItemChanged;
  209. /// <summary>
  210. /// The currently selected item from the list of radio labels
  211. /// </summary>
  212. /// <value>The selected.</value>
  213. public int SelectedItem {
  214. get => selected;
  215. set {
  216. OnSelectedItemChanged (value, SelectedItem);
  217. cursor = selected;
  218. SetNeedsDisplay ();
  219. }
  220. }
  221. /// <summary>
  222. /// Called whenever the current selected item changes. Invokes the <see cref="SelectedItemChanged"/> event.
  223. /// </summary>
  224. /// <param name="selectedItem"></param>
  225. /// <param name="previousSelectedItem"></param>
  226. public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
  227. {
  228. selected = selectedItem;
  229. SelectedItemChanged?.Invoke (new SelectedItemChangedArgs (selectedItem, previousSelectedItem));
  230. }
  231. ///<inheritdoc/>
  232. public override bool ProcessColdKey (KeyEvent kb)
  233. {
  234. var key = kb.KeyValue;
  235. if (key < Char.MaxValue && Char.IsLetterOrDigit ((char)key)) {
  236. int i = 0;
  237. key = Char.ToUpper ((char)key);
  238. foreach (var l in radioLabels) {
  239. bool nextIsHot = false;
  240. foreach (var c in l) {
  241. if (c == '_')
  242. nextIsHot = true;
  243. else {
  244. if (nextIsHot && c == key) {
  245. SelectedItem = i;
  246. cursor = i;
  247. if (!HasFocus)
  248. SetFocus ();
  249. return true;
  250. }
  251. nextIsHot = false;
  252. }
  253. }
  254. i++;
  255. }
  256. }
  257. return false;
  258. }
  259. ///<inheritdoc/>
  260. public override bool ProcessKey (KeyEvent kb)
  261. {
  262. switch (kb.Key) {
  263. case Key.CursorUp:
  264. if (cursor > 0) {
  265. cursor--;
  266. SetNeedsDisplay ();
  267. return true;
  268. }
  269. break;
  270. case Key.CursorDown:
  271. if (cursor + 1 < radioLabels.Count) {
  272. cursor++;
  273. SetNeedsDisplay ();
  274. return true;
  275. }
  276. break;
  277. case Key.Space:
  278. SelectedItem = cursor;
  279. return true;
  280. }
  281. return base.ProcessKey (kb);
  282. }
  283. ///<inheritdoc/>
  284. public override bool MouseEvent (MouseEvent me)
  285. {
  286. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)) {
  287. return false;
  288. }
  289. if (!CanFocus) {
  290. return false;
  291. }
  292. SetFocus ();
  293. var pos = displayMode == DisplayModeLayout.Horizontal ? me.X : me.Y;
  294. var rCount = displayMode == DisplayModeLayout.Horizontal ? horizontal.Last ().pos + horizontal.Last ().length : radioLabels.Count;
  295. if (pos < rCount) {
  296. var c = displayMode == DisplayModeLayout.Horizontal ? horizontal.FindIndex ((x) => x.pos <= me.X && x.pos + x.length - 2 >= me.X) : me.Y;
  297. if (c > -1) {
  298. cursor = SelectedItem = c;
  299. SetNeedsDisplay ();
  300. }
  301. }
  302. return true;
  303. }
  304. }
  305. /// <summary>
  306. /// Used for choose the display mode of this <see cref="RadioGroup"/>
  307. /// </summary>
  308. public enum DisplayModeLayout {
  309. /// <summary>
  310. /// Vertical mode display. It's the default.
  311. /// </summary>
  312. Vertical,
  313. /// <summary>
  314. /// Horizontal mode display.
  315. /// </summary>
  316. Horizontal
  317. }
  318. }