ComboBox.cs 22 KB

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