RadioGroup.cs 10 KB

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