ComboBox.cs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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 ((ustring)searchset [listview.SelectedItem]);
  105. };
  106. #if false
  107. Application.Loaded += (Application.ResizedEventArgs a) => {
  108. // Determine if this view is hosted inside a dialog
  109. for (View view = this.SuperView; view != null; view = view.SuperView) {
  110. if (view is Dialog) {
  111. autoHide = false;
  112. break;
  113. }
  114. }
  115. ResetSearchSet ();
  116. ColorScheme = autoHide ? Colors.Base : ColorScheme = null;
  117. // Needs to be re-applied for LayoutStyle.Computed
  118. // If Dim or Pos are null, these are the from the parametrized constructor
  119. listview.Y = 1;
  120. if (Width == null) {
  121. listview.Width = CalculateWidth ();
  122. search.Width = width;
  123. } else {
  124. width = GetDimAsInt (Width, vertical: false);
  125. search.Width = width;
  126. listview.Width = CalculateWidth ();
  127. }
  128. if (Height == null) {
  129. var h = CalculatetHeight ();
  130. listview.Height = h;
  131. this.Height = h + 1; // adjust view to account for search box
  132. } else {
  133. if (height == 0)
  134. height = GetDimAsInt (Height, vertical: true);
  135. listview.Height = CalculatetHeight ();
  136. this.Height = height + 1; // adjust view to account for search box
  137. }
  138. if (this.Text != null)
  139. Search_Changed (Text);
  140. if (autoHide)
  141. listview.ColorScheme = Colors.Menu;
  142. else
  143. search.ColorScheme = Colors.Menu;
  144. };
  145. #endif
  146. search.MouseClick += Search_MouseClick;
  147. this.Add(listview, search);
  148. this.SetFocus(search);
  149. }
  150. private void Search_MouseClick (MouseEventArgs e)
  151. {
  152. if (e.MouseEvent.Flags != MouseFlags.Button1Clicked)
  153. return;
  154. SuperView.SetFocus (search);
  155. }
  156. ///<inheritdoc/>
  157. public override bool OnEnter ()
  158. {
  159. if (!search.HasFocus)
  160. this.SetFocus (search);
  161. search.CursorPosition = search.Text.RuneCount;
  162. return true;
  163. }
  164. /// <summary>
  165. /// Invokes the SelectedChanged event if it is defined.
  166. /// </summary>
  167. /// <returns></returns>
  168. public virtual bool OnSelectedChanged ()
  169. {
  170. // Note: Cannot rely on "listview.SelectedItem != lastSelectedItem" because the list is dynamic.
  171. // So we cannot optimize. Ie: Don't call if not changed
  172. SelectedItemChanged?.Invoke (this, search.Text);
  173. return true;
  174. }
  175. ///<inheritdoc/>
  176. public override bool ProcessKey(KeyEvent e)
  177. {
  178. if (e.Key == Key.Tab) {
  179. base.ProcessKey(e);
  180. return false; // allow tab-out to next control
  181. }
  182. if (e.Key == Key.Enter && listview.HasFocus) {
  183. if (listview.Source.Count == 0 || searchset.Count == 0) {
  184. text = "";
  185. return true;
  186. }
  187. SetValue((ustring)searchset [listview.SelectedItem]);
  188. search.CursorPosition = search.Text.RuneCount;
  189. Search_Changed (search.Text);
  190. OnSelectedChanged ();
  191. searchset.Clear();
  192. listview.Clear ();
  193. listview.Height = 0;
  194. this.SetFocus(search);
  195. return true;
  196. }
  197. if (e.Key == Key.CursorDown && search.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) { // jump to list
  198. this.SetFocus (listview);
  199. SetValue ((ustring)searchset [listview.SelectedItem]);
  200. return true;
  201. }
  202. if (e.Key == Key.CursorUp && search.HasFocus) // stop odd behavior on KeyUp when search has focus
  203. return true;
  204. if (e.Key == Key.CursorUp && listview.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) // jump back to search
  205. {
  206. search.CursorPosition = search.Text.RuneCount;
  207. this.SetFocus (search);
  208. return true;
  209. }
  210. if (e.Key == Key.Esc) {
  211. this.SetFocus (search);
  212. search.Text = text = "";
  213. OnSelectedChanged ();
  214. return true;
  215. }
  216. // Unix emulation
  217. if (e.Key == Key.ControlU)
  218. {
  219. Reset();
  220. return true;
  221. }
  222. return base.ProcessKey(e);
  223. }
  224. /// <summary>
  225. /// The currently selected list item
  226. /// </summary>
  227. public new ustring Text
  228. {
  229. get
  230. {
  231. return text;
  232. }
  233. set {
  234. search.Text = text = value;
  235. }
  236. }
  237. private void SetValue(ustring text)
  238. {
  239. search.TextChanged -= Search_Changed;
  240. this.text = search.Text = text;
  241. search.CursorPosition = 0;
  242. search.TextChanged += Search_Changed;
  243. }
  244. /// <summary>
  245. /// Reset to full original list
  246. /// </summary>
  247. private void Reset()
  248. {
  249. search.Text = text = "";
  250. OnSelectedChanged();
  251. ResetSearchSet ();
  252. listview.SetSource(searchset);
  253. listview.Height = CalculatetHeight ();
  254. this.SetFocus(search);
  255. }
  256. private void ResetSearchSet()
  257. {
  258. if (autoHide) {
  259. if (searchset == null)
  260. searchset = new List<string> ();
  261. else
  262. searchset.Clear ();
  263. } else
  264. searchset = source.ToList ();
  265. }
  266. private void Search_Changed (ustring text)
  267. {
  268. if (source == null) // Object initialization
  269. return;
  270. if (ustring.IsNullOrEmpty (search.Text))
  271. ResetSearchSet ();
  272. else
  273. searchset = source.ToList().Cast<ustring>().Where (x => x.StartsWith (search.Text)).ToList();
  274. listview.SetSource (searchset);
  275. listview.Height = CalculatetHeight ();
  276. listview.Redraw (new Rect (0, 0, width, height)); // for any view behind this
  277. this.SuperView?.BringSubviewToFront (this);
  278. }
  279. /// <summary>
  280. /// Internal height of dynamic search list
  281. /// </summary>
  282. /// <returns></returns>
  283. private int CalculatetHeight ()
  284. {
  285. return Math.Min (height, searchset.Count);
  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. /// Get Dim as integer value
  297. /// </summary>
  298. /// <param name="dim"></param>
  299. /// <param name="vertical"></param>
  300. /// <returns></returns>n
  301. private int GetDimAsInt (Dim dim, bool vertical)
  302. {
  303. if (dim is Dim.DimAbsolute)
  304. return dim.Anchor (0);
  305. else { // Dim.Fill Dim.Factor
  306. if(autoHide)
  307. return vertical ? dim.Anchor (SuperView.Bounds.Height) : dim.Anchor (SuperView.Bounds.Width);
  308. else
  309. return vertical ? dim.Anchor (Bounds.Height) : dim.Anchor (Bounds.Width);
  310. }
  311. }
  312. }
  313. }