ComboBox.cs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. //
  2. // ComboBox.cs: ComboBox control
  3. //
  4. // Authors:
  5. // Ross Ferguson ([email protected])
  6. //
  7. using System;
  8. using System.Collections;
  9. using System.Collections.Generic;
  10. using System.Linq;
  11. using NStack;
  12. namespace Terminal.Gui {
  13. /// <summary>
  14. /// ComboBox control
  15. /// </summary>
  16. public class ComboBox : View {
  17. IListDataSource source;
  18. /// <summary>
  19. /// Gets or sets the <see cref="IListDataSource"/> backing this <see cref="ComboBox"/>, enabling custom rendering.
  20. /// </summary>
  21. /// <value>The source.</value>
  22. /// <remarks>
  23. /// Use <see cref="SetSource"/> to set a new <see cref="IList"/> source.
  24. /// </remarks>
  25. public IListDataSource Source {
  26. get => source;
  27. set {
  28. source = value;
  29. Search_Changed ("");
  30. SetNeedsDisplay ();
  31. }
  32. }
  33. /// <summary>
  34. /// Sets the source of the <see cref="ComboBox"/> to an <see cref="IList"/>.
  35. /// </summary>
  36. /// <value>An object implementing the IList interface.</value>
  37. /// <remarks>
  38. /// Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custome rendering.
  39. /// </remarks>
  40. public void SetSource (IList source)
  41. {
  42. if (source == null)
  43. Source = null;
  44. else {
  45. Source = MakeWrapper (source);
  46. }
  47. }
  48. /// <summary>
  49. /// Changed event, raised when the selection has been confirmed.
  50. /// </summary>
  51. /// <remarks>
  52. /// Client code can hook up to this event, it is
  53. /// raised when the selection has been confirmed.
  54. /// </remarks>
  55. public event EventHandler<ustring> SelectedItemChanged;
  56. IList searchset;
  57. ustring text = "";
  58. readonly TextField search;
  59. readonly ListView listview;
  60. int height;
  61. int width;
  62. bool autoHide = true;
  63. /// <summary>
  64. /// Public constructor
  65. /// </summary>
  66. public ComboBox () : base()
  67. {
  68. search = new TextField ("");
  69. listview = new ListView () { LayoutStyle = LayoutStyle.Computed, CanFocus = true };
  70. Initialize ();
  71. }
  72. /// <summary>
  73. /// Public constructor
  74. /// </summary>
  75. /// <param name="text"></param>
  76. public ComboBox (ustring text) : base ()
  77. {
  78. search = new TextField ("");
  79. listview = new ListView () { LayoutStyle = LayoutStyle.Computed, CanFocus = true };
  80. Initialize ();
  81. Text = text;
  82. }
  83. /// <summary>
  84. /// Public constructor
  85. /// </summary>
  86. /// <param name="text"></param>
  87. public ComboBox (ustring text) : base ()
  88. {
  89. search = new TextField ("");
  90. listview = new ListView () { LayoutStyle = LayoutStyle.Computed, CanFocus = true };
  91. Initialize ();
  92. Text = text;
  93. }
  94. /// <summary>
  95. /// Public constructor
  96. /// </summary>
  97. /// <param name="rect"></param>
  98. /// <param name="source"></param>
  99. public ComboBox (Rect rect, IList source) : base (rect)
  100. {
  101. this.height = rect.Height;
  102. this.width = rect.Width;
  103. search = new TextField ("") { Width = width };
  104. listview = new ListView (rect, source) { LayoutStyle = LayoutStyle.Computed };
  105. Initialize ();
  106. SetSource (source);
  107. }
  108. static IListDataSource MakeWrapper (IList source)
  109. {
  110. return new ListWrapper (source);
  111. }
  112. private void Initialize()
  113. {
  114. ColorScheme = Colors.Base;
  115. search.TextChanged += Search_Changed;
  116. listview.OpenSelectedItem += (ListViewItemEventArgs a) => Selected();
  117. // On resize
  118. LayoutComplete += (LayoutEventArgs a) => {
  119. search.Width = Bounds.Width;
  120. listview.Width = autoHide ? Bounds.Width - 1 : Bounds.Width;
  121. listview.Height = CalculatetHeight ();
  122. };
  123. listview.SelectedItemChanged += (ListViewItemEventArgs e) => {
  124. if(searchset.Count > 0)
  125. SetValue ((ustring)searchset [listview.SelectedItem]);
  126. };
  127. Application.Loaded += (Application.ResizedEventArgs a) => {
  128. // Determine if this view is hosted inside a dialog
  129. for (View view = this.SuperView; view != null; view = view.SuperView) {
  130. if (view is Dialog) {
  131. autoHide = false;
  132. break;
  133. }
  134. }
  135. ResetSearchSet ();
  136. ColorScheme = autoHide ? Colors.Base : ColorScheme = null;
  137. listview.Y = Pos.Bottom (search);
  138. if (Width != null && width == 0) // new ComboBox() { Width =
  139. width = Bounds.Width;
  140. search.Width = width;
  141. listview.Width = CalculateWidth ();
  142. if (Height != null && height == 0) // new ComboBox() { Height =
  143. height = Bounds.Height;
  144. listview.Height = CalculatetHeight ();
  145. SetNeedsLayout ();
  146. if (this.Text != null)
  147. Search_Changed (Text);
  148. if (autoHide)
  149. listview.ColorScheme = Colors.Menu;
  150. else
  151. search.ColorScheme = Colors.Menu;
  152. };
  153. search.MouseClick += Search_MouseClick;
  154. this.Add(listview, search);
  155. this.SetFocus(search);
  156. }
  157. private void Search_MouseClick (MouseEventArgs e)
  158. {
  159. if (e.MouseEvent.Flags != MouseFlags.Button1Clicked)
  160. return;
  161. SuperView.SetFocus (search);
  162. }
  163. ///<inheritdoc/>
  164. public override bool OnEnter ()
  165. {
  166. if (!search.HasFocus)
  167. this.SetFocus (search);
  168. search.CursorPosition = search.Text.Length;
  169. return true;
  170. }
  171. /// <summary>
  172. /// Invokes the SelectedChanged event if it is defined.
  173. /// </summary>
  174. /// <returns></returns>
  175. public virtual bool OnSelectedChanged ()
  176. {
  177. // Note: Cannot rely on "listview.SelectedItem != lastSelectedItem" because the list is dynamic.
  178. // So we cannot optimize. Ie: Don't call if not changed
  179. SelectedItemChanged?.Invoke (this, search.Text);
  180. return true;
  181. }
  182. ///<inheritdoc/>
  183. public override bool ProcessKey(KeyEvent e)
  184. {
  185. if (e.Key == Key.Tab) {
  186. base.ProcessKey(e);
  187. return false; // allow tab-out to next control
  188. }
  189. if (e.Key == Key.Enter && listview.HasFocus) {
  190. Selected ();
  191. return true;
  192. }
  193. if (e.Key == Key.CursorDown && search.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) { // jump to list
  194. this.SetFocus (listview);
  195. SetValue ((ustring)searchset [listview.SelectedItem]);
  196. return true;
  197. }
  198. if (e.Key == Key.CursorUp && search.HasFocus) // stop odd behavior on KeyUp when search has focus
  199. return true;
  200. if (e.Key == Key.CursorUp && listview.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) // jump back to search
  201. {
  202. search.CursorPosition = search.Text.Length;
  203. this.SetFocus (search);
  204. return true;
  205. }
  206. if (e.Key == Key.Esc) {
  207. this.SetFocus (search);
  208. search.Text = text = "";
  209. OnSelectedChanged ();
  210. return true;
  211. }
  212. // Unix emulation
  213. if (e.Key == Key.ControlU)
  214. {
  215. Reset();
  216. return true;
  217. }
  218. return base.ProcessKey(e);
  219. }
  220. /// <summary>
  221. /// The currently selected list item
  222. /// </summary>
  223. public ustring Text
  224. {
  225. get
  226. {
  227. return text;
  228. }
  229. set {
  230. search.Text = text = value;
  231. }
  232. }
  233. private void SetValue(ustring text)
  234. {
  235. search.TextChanged -= Search_Changed;
  236. this.text = search.Text = text;
  237. search.CursorPosition = 0;
  238. search.TextChanged += Search_Changed;
  239. }
  240. private void Selected()
  241. {
  242. if (listview.Source.Count == 0 || searchset.Count == 0) {
  243. text = "";
  244. return;
  245. }
  246. SetValue ((ustring)searchset [listview.SelectedItem]);
  247. search.CursorPosition = search.Text.Length;
  248. Search_Changed (search.Text);
  249. Reset (keepSearchText: true);
  250. }
  251. /// <summary>
  252. /// Reset to full original list
  253. /// </summary>
  254. private void Reset(bool keepSearchText = false)
  255. {
  256. if(!keepSearchText)
  257. search.Text = text = "";
  258. OnSelectedChanged ();
  259. ResetSearchSet ();
  260. listview.SetSource(searchset);
  261. listview.Height = CalculatetHeight ();
  262. this.SetFocus(search);
  263. }
  264. private void ResetSearchSet()
  265. {
  266. if (autoHide) {
  267. if (searchset == null)
  268. searchset = new List<string> ();
  269. else
  270. searchset.Clear ();
  271. } else
  272. searchset = source.ToList ();
  273. }
  274. private void Search_Changed (ustring text)
  275. {
  276. if (source == null) // Object initialization
  277. return;
  278. if (string.IsNullOrEmpty (search.Text.ToString ()))
  279. ResetSearchSet ();
  280. else
  281. searchset = source.ToList().Cast<object>().Where (x => x.ToString().StartsWith (search.Text.ToString (), StringComparison.CurrentCultureIgnoreCase)).ToList();
  282. listview.SetSource (searchset);
  283. listview.Height = CalculatetHeight ();
  284. listview.Redraw (new Rect (0, 0, width, height)); // for any view behind this
  285. this.SuperView?.BringSubviewToFront (this);
  286. }
  287. /// <summary>
  288. /// Internal width of search list
  289. /// </summary>
  290. /// <returns></returns>
  291. private int CalculateWidth ()
  292. {
  293. return autoHide ? Math.Max (1, width - 1) : width;
  294. }
  295. /// <summary>
  296. /// Internal height of dynamic search list
  297. /// </summary>
  298. /// <returns></returns>
  299. private int CalculatetHeight ()
  300. {
  301. var h = (Height is Dim.DimAbsolute) ? height : Bounds.Height;
  302. return Math.Min (Math.Max (0, h - 1), searchset?.Count ?? 0);
  303. }
  304. }
  305. }