ComboBox.cs 8.7 KB

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