RadioGroup.cs 13 KB

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