RadioGroup.cs 12 KB

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