RadioGroup.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  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. /// Allow to invoke the <see cref="SelectedItemChanged"/> after their creation.
  237. /// </summary>
  238. public void Refresh ()
  239. {
  240. OnSelectedItemChanged (selected, -1);
  241. }
  242. /// <summary>
  243. /// Called whenever the current selected item changes. Invokes the <see cref="SelectedItemChanged"/> event.
  244. /// </summary>
  245. /// <param name="selectedItem"></param>
  246. /// <param name="previousSelectedItem"></param>
  247. public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
  248. {
  249. selected = selectedItem;
  250. SelectedItemChanged?.Invoke (new SelectedItemChangedArgs (selectedItem, previousSelectedItem));
  251. }
  252. ///<inheritdoc/>
  253. public override bool ProcessColdKey (KeyEvent kb)
  254. {
  255. var key = kb.KeyValue;
  256. if (key < Char.MaxValue && Char.IsLetterOrDigit ((char)key)) {
  257. int i = 0;
  258. key = Char.ToUpper ((char)key);
  259. foreach (var l in radioLabels) {
  260. bool nextIsHot = false;
  261. foreach (var c in l) {
  262. if (c == '_')
  263. nextIsHot = true;
  264. else {
  265. if (nextIsHot && c == key) {
  266. SelectedItem = i;
  267. cursor = i;
  268. if (!HasFocus)
  269. SetFocus ();
  270. return true;
  271. }
  272. nextIsHot = false;
  273. }
  274. }
  275. i++;
  276. }
  277. }
  278. return false;
  279. }
  280. ///<inheritdoc/>
  281. public override bool ProcessKey (KeyEvent kb)
  282. {
  283. switch (kb.Key) {
  284. case Key.CursorUp:
  285. if (cursor > 0) {
  286. cursor--;
  287. SetNeedsDisplay ();
  288. return true;
  289. }
  290. break;
  291. case Key.CursorDown:
  292. if (cursor + 1 < radioLabels.Count) {
  293. cursor++;
  294. SetNeedsDisplay ();
  295. return true;
  296. }
  297. break;
  298. case Key.Space:
  299. SelectedItem = cursor;
  300. return true;
  301. }
  302. return base.ProcessKey (kb);
  303. }
  304. ///<inheritdoc/>
  305. public override bool MouseEvent (MouseEvent me)
  306. {
  307. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)) {
  308. return false;
  309. }
  310. if (!CanFocus) {
  311. return false;
  312. }
  313. SetFocus ();
  314. var pos = displayMode == DisplayModeLayout.Horizontal ? me.X : me.Y;
  315. var rCount = displayMode == DisplayModeLayout.Horizontal ? horizontal.Last ().pos + horizontal.Last ().length : radioLabels.Count;
  316. if (pos < rCount) {
  317. var c = displayMode == DisplayModeLayout.Horizontal ? horizontal.FindIndex ((x) => x.pos <= me.X && x.pos + x.length - 2 >= me.X) : me.Y;
  318. if (c > -1) {
  319. cursor = SelectedItem = c;
  320. SetNeedsDisplay ();
  321. }
  322. }
  323. return true;
  324. }
  325. ///<inheritdoc/>
  326. public override bool OnEnter (View view)
  327. {
  328. Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
  329. return base.OnEnter (view);
  330. }
  331. }
  332. /// <summary>
  333. /// Used for choose the display mode of this <see cref="RadioGroup"/>
  334. /// </summary>
  335. public enum DisplayModeLayout {
  336. /// <summary>
  337. /// Vertical mode display. It's the default.
  338. /// </summary>
  339. Vertical,
  340. /// <summary>
  341. /// Horizontal mode display.
  342. /// </summary>
  343. Horizontal
  344. }
  345. }