RadioGroup.cs 11 KB

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