ComboBox.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  1. //
  2. // ComboBox.cs: ComboBox control
  3. //
  4. // Authors:
  5. // Ross Ferguson ([email protected])
  6. //
  7. using NStack;
  8. using System;
  9. using System.Collections;
  10. using System.Collections.Generic;
  11. namespace Terminal.Gui {
  12. /// <summary>
  13. /// Provides a drop-down list of items the user can select from.
  14. /// </summary>
  15. public class ComboBox : View {
  16. private class ComboListView : ListView {
  17. private int highlighted = -1;
  18. private bool isFocusing;
  19. private ComboBox container;
  20. private bool hideDropdownListOnClick;
  21. public ComboListView (ComboBox container, bool hideDropdownListOnClick)
  22. {
  23. Initialize (container, hideDropdownListOnClick);
  24. }
  25. public ComboListView (ComboBox container, Rect rect, IList source, bool hideDropdownListOnClick) : base (rect, source)
  26. {
  27. Initialize (container, hideDropdownListOnClick);
  28. }
  29. public ComboListView (ComboBox container, IList source, bool hideDropdownListOnClick) : base (source)
  30. {
  31. Initialize (container, hideDropdownListOnClick);
  32. }
  33. private void Initialize (ComboBox container, bool hideDropdownListOnClick)
  34. {
  35. this.container = container ?? throw new ArgumentNullException (nameof(container), "ComboBox container cannot be null.");
  36. HideDropdownListOnClick = hideDropdownListOnClick;
  37. }
  38. public bool HideDropdownListOnClick {
  39. get => hideDropdownListOnClick;
  40. set => hideDropdownListOnClick = WantContinuousButtonPressed = value;
  41. }
  42. public override bool MouseEvent (MouseEvent me)
  43. {
  44. var res = false;
  45. var isMousePositionValid = IsMousePositionValid (me);
  46. if (isMousePositionValid) {
  47. res = base.MouseEvent (me);
  48. }
  49. if (HideDropdownListOnClick && me.Flags == MouseFlags.Button1Clicked) {
  50. if (!isMousePositionValid && !isFocusing) {
  51. container.isShow = false;
  52. container.HideList ();
  53. } else if (isMousePositionValid) {
  54. OnOpenSelectedItem ();
  55. } else {
  56. isFocusing = false;
  57. }
  58. return true;
  59. } else if (me.Flags == MouseFlags.ReportMousePosition && HideDropdownListOnClick) {
  60. if (isMousePositionValid) {
  61. highlighted = Math.Min (TopItem + me.Y, Source.Count);
  62. SetNeedsDisplay ();
  63. }
  64. isFocusing = false;
  65. return true;
  66. }
  67. return res;
  68. }
  69. private bool IsMousePositionValid (MouseEvent me)
  70. {
  71. if (me.X >= 0 && me.X < Frame.Width && me.Y >= 0 && me.Y < Frame.Height) {
  72. return true;
  73. }
  74. return false;
  75. }
  76. public override void Redraw (Rect bounds)
  77. {
  78. var current = ColorScheme.Focus;
  79. Driver.SetAttribute (current);
  80. Move (0, 0);
  81. var f = Frame;
  82. var item = TopItem;
  83. bool focused = HasFocus;
  84. int col = AllowsMarking ? 2 : 0;
  85. int start = LeftItem;
  86. for (int row = 0; row < f.Height; row++, item++) {
  87. bool isSelected = item == container.SelectedItem;
  88. bool isHighlighted = hideDropdownListOnClick && item == highlighted;
  89. Attribute newcolor;
  90. if (isHighlighted || (isSelected && !hideDropdownListOnClick)) {
  91. newcolor = focused ? ColorScheme.Focus : ColorScheme.HotNormal;
  92. } else if (isSelected && hideDropdownListOnClick) {
  93. newcolor = focused ? ColorScheme.HotFocus : ColorScheme.HotNormal;
  94. } else {
  95. newcolor = focused ? GetNormalColor () : GetNormalColor ();
  96. }
  97. if (newcolor != current) {
  98. Driver.SetAttribute (newcolor);
  99. current = newcolor;
  100. }
  101. Move (0, row);
  102. if (Source == null || item >= Source.Count) {
  103. for (int c = 0; c < f.Width; c++)
  104. Driver.AddRune (' ');
  105. } else {
  106. var rowEventArgs = new ListViewRowEventArgs (item);
  107. OnRowRender (rowEventArgs);
  108. if (rowEventArgs.RowAttribute != null && current != rowEventArgs.RowAttribute) {
  109. current = (Attribute)rowEventArgs.RowAttribute;
  110. Driver.SetAttribute (current);
  111. }
  112. if (AllowsMarking) {
  113. Driver.AddRune (Source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
  114. Driver.AddRune (' ');
  115. }
  116. Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start);
  117. }
  118. }
  119. }
  120. public override bool OnEnter (View view)
  121. {
  122. if (hideDropdownListOnClick) {
  123. isFocusing = true;
  124. highlighted = container.SelectedItem;
  125. Application.GrabMouse (this);
  126. }
  127. return base.OnEnter (view);
  128. }
  129. public override bool OnLeave (View view)
  130. {
  131. if (hideDropdownListOnClick) {
  132. isFocusing = false;
  133. highlighted = container.SelectedItem;
  134. Application.UngrabMouse ();
  135. }
  136. return base.OnLeave (view);
  137. }
  138. public override bool OnSelectedChanged ()
  139. {
  140. var res = base.OnSelectedChanged ();
  141. highlighted = SelectedItem;
  142. return res;
  143. }
  144. }
  145. IListDataSource source;
  146. /// <summary>
  147. /// Gets or sets the <see cref="IListDataSource"/> backing this <see cref="ComboBox"/>, enabling custom rendering.
  148. /// </summary>
  149. /// <value>The source.</value>
  150. /// <remarks>
  151. /// Use <see cref="SetSource"/> to set a new <see cref="IList"/> source.
  152. /// </remarks>
  153. public IListDataSource Source {
  154. get => source;
  155. set {
  156. source = value;
  157. // Only need to refresh list if its been added to a container view
  158. if (SuperView != null && SuperView.Subviews.Contains (this)) {
  159. SelectedItem = -1;
  160. search.Text = "";
  161. Search_Changed ("");
  162. SetNeedsDisplay ();
  163. }
  164. }
  165. }
  166. /// <summary>
  167. /// Sets the source of the <see cref="ComboBox"/> to an <see cref="IList"/>.
  168. /// </summary>
  169. /// <value>An object implementing the IList interface.</value>
  170. /// <remarks>
  171. /// Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custome rendering.
  172. /// </remarks>
  173. public void SetSource (IList source)
  174. {
  175. if (source == null) {
  176. Source = null;
  177. } else {
  178. listview.SetSource (source);
  179. Source = listview.Source;
  180. }
  181. }
  182. /// <summary>
  183. /// This event is raised when the selected item in the <see cref="ComboBox"/> has changed.
  184. /// </summary>
  185. public event Action<ListViewItemEventArgs> SelectedItemChanged;
  186. /// <summary>
  187. /// This event is raised when the drop-down list is expanded.
  188. /// </summary>
  189. public event Action Expanded;
  190. /// <summary>
  191. /// This event is raised when the drop-down list is collapsed.
  192. /// </summary>
  193. public event Action Collapsed;
  194. /// <summary>
  195. /// This event is raised when the user Double Clicks on an item or presses ENTER to open the selected item.
  196. /// </summary>
  197. public event Action<ListViewItemEventArgs> OpenSelectedItem;
  198. readonly IList searchset = new List<object> ();
  199. ustring text = "";
  200. readonly TextField search;
  201. readonly ComboListView listview;
  202. bool autoHide = true;
  203. readonly int minimumHeight = 2;
  204. /// <summary>
  205. /// Public constructor
  206. /// </summary>
  207. public ComboBox () : this (string.Empty)
  208. {
  209. }
  210. /// <summary>
  211. /// Public constructor
  212. /// </summary>
  213. /// <param name="text"></param>
  214. public ComboBox (ustring text) : base ()
  215. {
  216. search = new TextField ("");
  217. listview = new ComboListView (this, HideDropdownListOnClick) { LayoutStyle = LayoutStyle.Computed, CanFocus = true, TabStop = false };
  218. Initialize ();
  219. Text = text;
  220. }
  221. /// <summary>
  222. /// Public constructor
  223. /// </summary>
  224. /// <param name="rect"></param>
  225. /// <param name="source"></param>
  226. public ComboBox (Rect rect, IList source) : base (rect)
  227. {
  228. search = new TextField ("") { Width = rect.Width };
  229. listview = new ComboListView (this, rect, source, HideDropdownListOnClick) { LayoutStyle = LayoutStyle.Computed, ColorScheme = Colors.Base };
  230. Initialize ();
  231. SetSource (source);
  232. }
  233. /// <summary>
  234. /// Initialize with the source.
  235. /// </summary>
  236. /// <param name="source">The source.</param>
  237. public ComboBox (IList source) : this (string.Empty)
  238. {
  239. search = new TextField ("");
  240. listview = new ComboListView (this, source, HideDropdownListOnClick) { LayoutStyle = LayoutStyle.Computed, ColorScheme = Colors.Base };
  241. Initialize ();
  242. SetSource (source);
  243. }
  244. private void Initialize ()
  245. {
  246. if (Bounds.Height < minimumHeight && (Height == null || Height is Dim.DimAbsolute)) {
  247. Height = minimumHeight;
  248. }
  249. search.TextChanged += Search_Changed;
  250. listview.Y = Pos.Bottom (search);
  251. listview.OpenSelectedItem += (ListViewItemEventArgs a) => Selected ();
  252. this.Add (search, listview);
  253. // On resize
  254. LayoutComplete += (LayoutEventArgs a) => {
  255. if ((!autoHide && Bounds.Width > 0 && search.Frame.Width != Bounds.Width) ||
  256. (autoHide && Bounds.Width > 0 && search.Frame.Width != Bounds.Width - 1)) {
  257. search.Width = listview.Width = autoHide ? Bounds.Width - 1 : Bounds.Width;
  258. listview.Height = CalculatetHeight ();
  259. search.SetRelativeLayout (Bounds);
  260. listview.SetRelativeLayout (Bounds);
  261. }
  262. };
  263. listview.SelectedItemChanged += (ListViewItemEventArgs e) => {
  264. if (!HideDropdownListOnClick && searchset.Count > 0) {
  265. SetValue (searchset [listview.SelectedItem]);
  266. }
  267. };
  268. Added += (View v) => {
  269. // Determine if this view is hosted inside a dialog and is the only control
  270. for (View view = this.SuperView; view != null; view = view.SuperView) {
  271. if (view is Dialog && SuperView != null && SuperView.Subviews.Count == 1 && SuperView.Subviews [0] == this) {
  272. autoHide = false;
  273. break;
  274. }
  275. }
  276. SetNeedsLayout ();
  277. SetNeedsDisplay ();
  278. Search_Changed (Text);
  279. };
  280. // Things this view knows how to do
  281. AddCommand (Command.Accept, () => ActivateSelected ());
  282. AddCommand (Command.ToggleExpandCollapse, () => ExpandCollapse ());
  283. AddCommand (Command.Expand, () => Expand ());
  284. AddCommand (Command.Collapse, () => Collapse ());
  285. AddCommand (Command.LineDown, () => MoveDown ());
  286. AddCommand (Command.LineUp, () => MoveUp ());
  287. AddCommand (Command.PageDown, () => PageDown ());
  288. AddCommand (Command.PageUp, () => PageUp ());
  289. AddCommand (Command.TopHome, () => MoveHome ());
  290. AddCommand (Command.BottomEnd, () => MoveEnd ());
  291. AddCommand (Command.Cancel, () => CancelSelected ());
  292. AddCommand (Command.UnixEmulation, () => UnixEmulation ());
  293. // Default keybindings for this view
  294. AddKeyBinding (Key.Enter, Command.Accept);
  295. AddKeyBinding (Key.F4, Command.ToggleExpandCollapse);
  296. AddKeyBinding (Key.CursorDown, Command.LineDown);
  297. AddKeyBinding (Key.CursorUp, Command.LineUp);
  298. AddKeyBinding (Key.PageDown, Command.PageDown);
  299. AddKeyBinding (Key.PageUp, Command.PageUp);
  300. AddKeyBinding (Key.Home, Command.TopHome);
  301. AddKeyBinding (Key.End, Command.BottomEnd);
  302. AddKeyBinding (Key.Esc, Command.Cancel);
  303. AddKeyBinding (Key.U | Key.CtrlMask, Command.UnixEmulation);
  304. }
  305. private bool isShow = false;
  306. private int selectedItem = -1;
  307. private int lastSelectedItem = -1;
  308. private bool hideDropdownListOnClick;
  309. /// <summary>
  310. /// Gets the index of the currently selected item in the <see cref="Source"/>
  311. /// </summary>
  312. /// <value>The selected item or -1 none selected.</value>
  313. public int SelectedItem {
  314. get => selectedItem;
  315. set {
  316. if (selectedItem != value && (value == -1
  317. || (source != null && value > -1 && value < source.Count))) {
  318. selectedItem = lastSelectedItem = value;
  319. if (selectedItem != -1) {
  320. SetValue (source.ToList () [selectedItem].ToString (), true);
  321. } else {
  322. SetValue ("", true);
  323. }
  324. OnSelectedChanged ();
  325. }
  326. }
  327. }
  328. /// <summary>
  329. /// Gets the drop down list state, expanded or collapsed.
  330. /// </summary>
  331. public bool IsShow => isShow;
  332. ///<inheritdoc/>
  333. public new ColorScheme ColorScheme {
  334. get {
  335. return base.ColorScheme;
  336. }
  337. set {
  338. listview.ColorScheme = value;
  339. base.ColorScheme = value;
  340. SetNeedsDisplay ();
  341. }
  342. }
  343. /// <summary>
  344. ///If set to true its not allow any changes in the text.
  345. /// </summary>
  346. public bool ReadOnly {
  347. get => search.ReadOnly;
  348. set {
  349. search.ReadOnly = value;
  350. if (search.ReadOnly) {
  351. if (search.ColorScheme != null) {
  352. search.ColorScheme.Normal = search.ColorScheme.Focus;
  353. }
  354. }
  355. }
  356. }
  357. /// <summary>
  358. /// Gets or sets if the drop-down list can be hide with a button click event.
  359. /// </summary>
  360. public bool HideDropdownListOnClick {
  361. get => hideDropdownListOnClick;
  362. set => hideDropdownListOnClick = listview.HideDropdownListOnClick = value;
  363. }
  364. ///<inheritdoc/>
  365. public override bool MouseEvent (MouseEvent me)
  366. {
  367. if (me.X == Bounds.Right - 1 && me.Y == Bounds.Top && me.Flags == MouseFlags.Button1Pressed
  368. && autoHide) {
  369. if (isShow) {
  370. isShow = false;
  371. HideList ();
  372. } else {
  373. SetSearchSet ();
  374. isShow = true;
  375. ShowList ();
  376. FocusSelectedItem ();
  377. }
  378. return true;
  379. } else if (me.Flags == MouseFlags.Button1Pressed) {
  380. if (!search.HasFocus) {
  381. search.SetFocus ();
  382. }
  383. return true;
  384. }
  385. return false;
  386. }
  387. private void FocusSelectedItem ()
  388. {
  389. listview.SelectedItem = SelectedItem > -1 ? SelectedItem : 0;
  390. listview.TabStop = true;
  391. listview.SetFocus ();
  392. OnExpanded ();
  393. }
  394. /// <summary>
  395. /// Virtual method which invokes the <see cref="Expanded"/> event.
  396. /// </summary>
  397. public virtual void OnExpanded ()
  398. {
  399. Expanded?.Invoke ();
  400. }
  401. /// <summary>
  402. /// Virtual method which invokes the <see cref="Collapsed"/> event.
  403. /// </summary>
  404. public virtual void OnCollapsed ()
  405. {
  406. Collapsed?.Invoke ();
  407. }
  408. ///<inheritdoc/>
  409. public override bool OnEnter (View view)
  410. {
  411. if (!search.HasFocus && !listview.HasFocus) {
  412. search.SetFocus ();
  413. }
  414. search.CursorPosition = search.Text.RuneCount;
  415. return base.OnEnter (view);
  416. }
  417. ///<inheritdoc/>
  418. public override bool OnLeave (View view)
  419. {
  420. if (source?.Count > 0 && selectedItem > -1 && selectedItem < source.Count - 1
  421. && text != source.ToList () [selectedItem].ToString ()) {
  422. SetValue (source.ToList () [selectedItem].ToString ());
  423. }
  424. if (autoHide && isShow && view != this && view != search && view != listview) {
  425. isShow = false;
  426. HideList ();
  427. } else if (listview.TabStop) {
  428. listview.TabStop = false;
  429. }
  430. return base.OnLeave (view);
  431. }
  432. /// <summary>
  433. /// Invokes the SelectedChanged event if it is defined.
  434. /// </summary>
  435. /// <returns></returns>
  436. public virtual bool OnSelectedChanged ()
  437. {
  438. // Note: Cannot rely on "listview.SelectedItem != lastSelectedItem" because the list is dynamic.
  439. // So we cannot optimize. Ie: Don't call if not changed
  440. SelectedItemChanged?.Invoke (new ListViewItemEventArgs (SelectedItem, search.Text));
  441. return true;
  442. }
  443. /// <summary>
  444. /// Invokes the OnOpenSelectedItem event if it is defined.
  445. /// </summary>
  446. /// <returns></returns>
  447. public virtual bool OnOpenSelectedItem ()
  448. {
  449. var value = search.Text;
  450. lastSelectedItem = SelectedItem;
  451. OpenSelectedItem?.Invoke (new ListViewItemEventArgs (SelectedItem, value));
  452. return true;
  453. }
  454. ///<inheritdoc/>
  455. public override void Redraw (Rect bounds)
  456. {
  457. base.Redraw (bounds);
  458. if (!autoHide) {
  459. return;
  460. }
  461. Driver.SetAttribute (ColorScheme.Focus);
  462. Move (Bounds.Right - 1, 0);
  463. Driver.AddRune (Driver.DownArrow);
  464. }
  465. ///<inheritdoc/>
  466. public override bool ProcessKey (KeyEvent e)
  467. {
  468. var result = InvokeKeybindings (e);
  469. if (result != null)
  470. return (bool)result;
  471. return base.ProcessKey (e);
  472. }
  473. bool UnixEmulation ()
  474. {
  475. // Unix emulation
  476. Reset ();
  477. return true;
  478. }
  479. bool CancelSelected ()
  480. {
  481. search.SetFocus ();
  482. if (ReadOnly || HideDropdownListOnClick) {
  483. SelectedItem = lastSelectedItem;
  484. if (SelectedItem > -1 && listview.Source?.Count > 0) {
  485. search.Text = text = listview.Source.ToList () [SelectedItem].ToString ();
  486. }
  487. } else if (!ReadOnly) {
  488. search.Text = text = "";
  489. selectedItem = lastSelectedItem;
  490. OnSelectedChanged ();
  491. }
  492. Collapse ();
  493. return true;
  494. }
  495. bool? MoveEnd ()
  496. {
  497. if (!isShow && search.HasFocus) {
  498. return null;
  499. }
  500. if (HasItems ()) {
  501. listview.MoveEnd ();
  502. }
  503. return true;
  504. }
  505. bool? MoveHome ()
  506. {
  507. if (!isShow && search.HasFocus) {
  508. return null;
  509. }
  510. if (HasItems ()) {
  511. listview.MoveHome ();
  512. }
  513. return true;
  514. }
  515. bool PageUp ()
  516. {
  517. if (HasItems ()) {
  518. listview.MovePageUp ();
  519. }
  520. return true;
  521. }
  522. bool PageDown ()
  523. {
  524. if (HasItems ()) {
  525. listview.MovePageDown ();
  526. }
  527. return true;
  528. }
  529. bool? MoveUp ()
  530. {
  531. if (search.HasFocus) { // stop odd behavior on KeyUp when search has focus
  532. return true;
  533. }
  534. if (listview.HasFocus && listview.SelectedItem == 0 && searchset?.Count > 0) // jump back to search
  535. {
  536. search.CursorPosition = search.Text.RuneCount;
  537. search.SetFocus ();
  538. return true;
  539. }
  540. return null;
  541. }
  542. bool? MoveDown ()
  543. {
  544. if (search.HasFocus) { // jump to list
  545. if (searchset?.Count > 0) {
  546. listview.TabStop = true;
  547. listview.SetFocus ();
  548. SetValue (searchset [listview.SelectedItem]);
  549. } else {
  550. listview.TabStop = false;
  551. SuperView?.FocusNext ();
  552. }
  553. return true;
  554. }
  555. return null;
  556. }
  557. /// <summary>
  558. /// Toggles the expand/collapse state of the sublist in the combo box
  559. /// </summary>
  560. /// <returns></returns>
  561. bool ExpandCollapse ()
  562. {
  563. if (search.HasFocus || listview.HasFocus) {
  564. if (!isShow) {
  565. return Expand ();
  566. } else {
  567. return Collapse ();
  568. }
  569. }
  570. return false;
  571. }
  572. bool ActivateSelected ()
  573. {
  574. if (HasItems ()) {
  575. Selected ();
  576. return true;
  577. }
  578. return false;
  579. }
  580. bool HasItems ()
  581. {
  582. return Source?.Count > 0;
  583. }
  584. /// <summary>
  585. /// Collapses the drop down list. Returns true if the state chagned or false
  586. /// if it was already collapsed and no action was taken
  587. /// </summary>
  588. public virtual bool Collapse ()
  589. {
  590. if (!isShow) {
  591. return false;
  592. }
  593. isShow = false;
  594. HideList ();
  595. return true;
  596. }
  597. /// <summary>
  598. /// Expands the drop down list. Returns true if the state chagned or false
  599. /// if it was already expanded and no action was taken
  600. /// </summary>
  601. public virtual bool Expand ()
  602. {
  603. if (isShow) {
  604. return false;
  605. }
  606. SetSearchSet ();
  607. isShow = true;
  608. ShowList ();
  609. FocusSelectedItem ();
  610. return true;
  611. }
  612. /// <summary>
  613. /// The currently selected list item
  614. /// </summary>
  615. public new ustring Text {
  616. get {
  617. return text;
  618. }
  619. set {
  620. SetSearchText (value);
  621. }
  622. }
  623. /// <summary>
  624. /// Current search text
  625. /// </summary>
  626. public ustring SearchText {
  627. get {
  628. return search.Text;
  629. }
  630. set {
  631. SetSearchText (value);
  632. }
  633. }
  634. private void SetValue (object text, bool isFromSelectedItem = false)
  635. {
  636. search.TextChanged -= Search_Changed;
  637. this.text = search.Text = text.ToString ();
  638. search.CursorPosition = 0;
  639. search.TextChanged += Search_Changed;
  640. if (!isFromSelectedItem) {
  641. selectedItem = GetSelectedItemFromSource (this.text);
  642. OnSelectedChanged ();
  643. }
  644. }
  645. private void Selected ()
  646. {
  647. isShow = false;
  648. listview.TabStop = false;
  649. if (listview.Source.Count == 0 || (searchset?.Count ?? 0) == 0) {
  650. text = "";
  651. HideList ();
  652. return;
  653. }
  654. SetValue (searchset [listview.SelectedItem]);
  655. search.CursorPosition = search.Text.ConsoleWidth;
  656. Search_Changed (search.Text);
  657. OnOpenSelectedItem ();
  658. Reset (keepSearchText: true);
  659. HideList ();
  660. }
  661. private int GetSelectedItemFromSource (ustring value)
  662. {
  663. if (source == null) {
  664. return -1;
  665. }
  666. for (int i = 0; i < source.Count; i++) {
  667. if (source.ToList () [i].ToString () == value) {
  668. return i;
  669. }
  670. }
  671. return -1;
  672. }
  673. /// <summary>
  674. /// Reset to full original list
  675. /// </summary>
  676. private void Reset (bool keepSearchText = false)
  677. {
  678. if (!keepSearchText) {
  679. SetSearchText (string.Empty);
  680. }
  681. ResetSearchSet ();
  682. listview.SetSource (searchset);
  683. listview.Height = CalculatetHeight ();
  684. if (Subviews.Count > 0) {
  685. search.SetFocus ();
  686. }
  687. }
  688. private void SetSearchText (ustring value)
  689. {
  690. search.Text = text = value;
  691. }
  692. private void ResetSearchSet (bool noCopy = false)
  693. {
  694. searchset.Clear ();
  695. if (autoHide || noCopy)
  696. return;
  697. SetSearchSet ();
  698. }
  699. private void SetSearchSet ()
  700. {
  701. if (Source == null) { return; }
  702. // force deep copy
  703. foreach (var item in Source.ToList ()) {
  704. searchset.Add (item);
  705. }
  706. }
  707. private void Search_Changed (ustring text)
  708. {
  709. if (source == null) { // Object initialization
  710. return;
  711. }
  712. if (ustring.IsNullOrEmpty (search.Text) && ustring.IsNullOrEmpty (text)) {
  713. ResetSearchSet ();
  714. } else if (search.Text != text) {
  715. isShow = true;
  716. ResetSearchSet (noCopy: true);
  717. foreach (var item in source.ToList ()) { // Iterate to preserver object type and force deep copy
  718. if (item.ToString ().StartsWith (search.Text.ToString (), StringComparison.CurrentCultureIgnoreCase)) {
  719. searchset.Add (item);
  720. }
  721. }
  722. }
  723. if (HasFocus) {
  724. ShowList ();
  725. } else if (autoHide) {
  726. isShow = false;
  727. HideList ();
  728. }
  729. }
  730. /// <summary>
  731. /// Show the search list
  732. /// </summary>
  733. ///
  734. /// Consider making public
  735. private void ShowList ()
  736. {
  737. listview.SetSource (searchset);
  738. listview.Clear (); // Ensure list shrinks in Dialog as you type
  739. listview.Height = CalculatetHeight ();
  740. SuperView?.BringSubviewToFront (this);
  741. }
  742. /// <summary>
  743. /// Hide the search list
  744. /// </summary>
  745. ///
  746. /// Consider making public
  747. private void HideList ()
  748. {
  749. if (lastSelectedItem != selectedItem) {
  750. OnOpenSelectedItem ();
  751. }
  752. var rect = listview.ViewToScreen (listview.Bounds);
  753. Reset (keepSearchText: true);
  754. listview.Clear (rect);
  755. listview.TabStop = false;
  756. SuperView?.SendSubviewToBack (this);
  757. SuperView?.SetNeedsDisplay (rect);
  758. OnCollapsed ();
  759. }
  760. /// <summary>
  761. /// Internal height of dynamic search list
  762. /// </summary>
  763. /// <returns></returns>
  764. private int CalculatetHeight ()
  765. {
  766. if (Bounds.Height == 0)
  767. return 0;
  768. return Math.Min (Math.Max (Bounds.Height - 1, minimumHeight - 1), searchset?.Count > 0 ? searchset.Count : isShow ? Math.Max (Bounds.Height - 1, minimumHeight - 1) : 0);
  769. }
  770. }
  771. }