ComboBox.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  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.Text;
  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 OnDrawContent (Rect contentArea)
  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 ((Rune)' ');
  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 ? CM.Glyphs.Checked : CM.Glyphs.Selected) : (AllowsMultipleSelection ? CM.Glyphs.UnChecked : CM.Glyphs.UnSelected));
  114. Driver.AddRune ((Rune)' ');
  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 (this, new TextChangedEventArgs (""));
  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 EventHandler<ListViewItemEventArgs> SelectedItemChanged;
  186. /// <summary>
  187. /// This event is raised when the drop-down list is expanded.
  188. /// </summary>
  189. public event EventHandler Expanded;
  190. /// <summary>
  191. /// This event is raised when the drop-down list is collapsed.
  192. /// </summary>
  193. public event EventHandler 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 EventHandler<ListViewItemEventArgs> OpenSelectedItem;
  198. readonly IList searchset = new List<object> ();
  199. string 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 (string 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 += (object sender, ListViewItemEventArgs a) => Selected ();
  252. Add (search, listview);
  253. // On resize
  254. LayoutComplete += (object sender, 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 += (object sender, ListViewItemEventArgs e) => {
  264. if (!HideDropdownListOnClick && searchset.Count > 0) {
  265. SetValue (searchset [listview.SelectedItem]);
  266. }
  267. };
  268. Added += (s, e) => {
  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 (this, new TextChangedEventArgs (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. KeyBindings.Add (KeyCode.Enter, Command.Accept);
  295. KeyBindings.Add (KeyCode.F4, Command.ToggleExpandCollapse);
  296. KeyBindings.Add (KeyCode.CursorDown, Command.LineDown);
  297. KeyBindings.Add (KeyCode.CursorUp, Command.LineUp);
  298. KeyBindings.Add (KeyCode.PageDown, Command.PageDown);
  299. KeyBindings.Add (KeyCode.PageUp, Command.PageUp);
  300. KeyBindings.Add (KeyCode.Home, Command.TopHome);
  301. KeyBindings.Add (KeyCode.End, Command.BottomEnd);
  302. KeyBindings.Add (KeyCode.Esc, Command.Cancel);
  303. KeyBindings.Add (KeyCode.U | KeyCode.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 (this, EventArgs.Empty);
  400. }
  401. /// <summary>
  402. /// Virtual method which invokes the <see cref="Collapsed"/> event.
  403. /// </summary>
  404. public virtual void OnCollapsed ()
  405. {
  406. Collapsed?.Invoke (this, EventArgs.Empty);
  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.GetRuneCount ();
  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 (this, 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 (this, new ListViewItemEventArgs (SelectedItem, value));
  452. return true;
  453. }
  454. ///<inheritdoc/>
  455. public override void OnDrawContent (Rect contentArea)
  456. {
  457. base.OnDrawContent (contentArea);
  458. if (!autoHide) {
  459. return;
  460. }
  461. Driver.SetAttribute (ColorScheme.Focus);
  462. Move (Bounds.Right - 1, 0);
  463. Driver.AddRune (CM.Glyphs.DownArrow);
  464. }
  465. bool UnixEmulation ()
  466. {
  467. // Unix emulation
  468. Reset ();
  469. return true;
  470. }
  471. bool CancelSelected ()
  472. {
  473. search.SetFocus ();
  474. if (ReadOnly || HideDropdownListOnClick) {
  475. SelectedItem = lastSelectedItem;
  476. if (SelectedItem > -1 && listview.Source?.Count > 0) {
  477. search.Text = text = listview.Source.ToList () [SelectedItem].ToString ();
  478. }
  479. } else if (!ReadOnly) {
  480. search.Text = text = "";
  481. selectedItem = lastSelectedItem;
  482. OnSelectedChanged ();
  483. }
  484. Collapse ();
  485. return true;
  486. }
  487. bool? MoveEnd ()
  488. {
  489. if (!isShow && search.HasFocus) {
  490. return null;
  491. }
  492. if (HasItems ()) {
  493. listview.MoveEnd ();
  494. }
  495. return true;
  496. }
  497. bool? MoveHome ()
  498. {
  499. if (!isShow && search.HasFocus) {
  500. return null;
  501. }
  502. if (HasItems ()) {
  503. listview.MoveHome ();
  504. }
  505. return true;
  506. }
  507. bool PageUp ()
  508. {
  509. if (HasItems ()) {
  510. listview.MovePageUp ();
  511. }
  512. return true;
  513. }
  514. bool PageDown ()
  515. {
  516. if (HasItems ()) {
  517. listview.MovePageDown ();
  518. }
  519. return true;
  520. }
  521. bool? MoveUp ()
  522. {
  523. if (search.HasFocus) { // stop odd behavior on KeyUp when search has focus
  524. return true;
  525. }
  526. if (listview.HasFocus && listview.SelectedItem == 0 && searchset?.Count > 0) // jump back to search
  527. {
  528. search.CursorPosition = search.Text.GetRuneCount ();
  529. search.SetFocus ();
  530. return true;
  531. }
  532. return null;
  533. }
  534. bool? MoveDown ()
  535. {
  536. if (search.HasFocus) { // jump to list
  537. if (searchset?.Count > 0) {
  538. listview.TabStop = true;
  539. listview.SetFocus ();
  540. if (listview.SelectedItem > -1) {
  541. SetValue (searchset [listview.SelectedItem]);
  542. }
  543. } else {
  544. listview.TabStop = false;
  545. SuperView?.FocusNext ();
  546. }
  547. return true;
  548. }
  549. return null;
  550. }
  551. /// <summary>
  552. /// Toggles the expand/collapse state of the sublist in the combo box
  553. /// </summary>
  554. /// <returns></returns>
  555. bool ExpandCollapse ()
  556. {
  557. if (search.HasFocus || listview.HasFocus) {
  558. if (!isShow) {
  559. return Expand ();
  560. } else {
  561. return Collapse ();
  562. }
  563. }
  564. return false;
  565. }
  566. bool ActivateSelected ()
  567. {
  568. if (HasItems ()) {
  569. Selected ();
  570. return true;
  571. }
  572. return false;
  573. }
  574. bool HasItems ()
  575. {
  576. return Source?.Count > 0;
  577. }
  578. /// <summary>
  579. /// Collapses the drop down list. Returns true if the state chagned or false
  580. /// if it was already collapsed and no action was taken
  581. /// </summary>
  582. public virtual bool Collapse ()
  583. {
  584. if (!isShow) {
  585. return false;
  586. }
  587. isShow = false;
  588. HideList ();
  589. return true;
  590. }
  591. /// <summary>
  592. /// Expands the drop down list. Returns true if the state chagned or false
  593. /// if it was already expanded and no action was taken
  594. /// </summary>
  595. public virtual bool Expand ()
  596. {
  597. if (isShow) {
  598. return false;
  599. }
  600. SetSearchSet ();
  601. isShow = true;
  602. ShowList ();
  603. FocusSelectedItem ();
  604. return true;
  605. }
  606. /// <summary>
  607. /// The currently selected list item
  608. /// </summary>
  609. public new string Text {
  610. get {
  611. return text;
  612. }
  613. set {
  614. SetSearchText (value);
  615. }
  616. }
  617. /// <summary>
  618. /// Current search text
  619. /// </summary>
  620. public string SearchText {
  621. get {
  622. return search.Text;
  623. }
  624. set {
  625. SetSearchText (value);
  626. }
  627. }
  628. private void SetValue (object text, bool isFromSelectedItem = false)
  629. {
  630. search.TextChanged -= Search_Changed;
  631. this.text = search.Text = text.ToString ();
  632. search.CursorPosition = 0;
  633. search.TextChanged += Search_Changed;
  634. if (!isFromSelectedItem) {
  635. selectedItem = GetSelectedItemFromSource (this.text);
  636. OnSelectedChanged ();
  637. }
  638. }
  639. private void Selected ()
  640. {
  641. isShow = false;
  642. listview.TabStop = false;
  643. if (listview.Source.Count == 0 || (searchset?.Count ?? 0) == 0) {
  644. text = "";
  645. HideList ();
  646. isShow = false;
  647. return;
  648. }
  649. SetValue (listview.SelectedItem > -1 ? searchset [listview.SelectedItem] : text);
  650. search.CursorPosition = search.Text.GetColumns ();
  651. Search_Changed (this, new TextChangedEventArgs (search.Text));
  652. OnOpenSelectedItem ();
  653. Reset (keepSearchText: true);
  654. HideList ();
  655. isShow = false;
  656. }
  657. private int GetSelectedItemFromSource (string searchText)
  658. {
  659. if (source is null) {
  660. return -1;
  661. }
  662. for (int i = 0; i < searchset.Count; i++) {
  663. if (searchset [i].ToString () == searchText) {
  664. return i;
  665. }
  666. }
  667. return -1;
  668. }
  669. /// <summary>
  670. /// Reset to full original list
  671. /// </summary>
  672. private void Reset (bool keepSearchText = false)
  673. {
  674. if (!keepSearchText) {
  675. SetSearchText (string.Empty);
  676. }
  677. ResetSearchSet ();
  678. listview.SetSource (searchset);
  679. listview.Height = CalculatetHeight ();
  680. if (Subviews.Count > 0) {
  681. search.SetFocus ();
  682. }
  683. }
  684. private void SetSearchText (string value)
  685. {
  686. search.Text = text = value;
  687. }
  688. private void ResetSearchSet (bool noCopy = false)
  689. {
  690. searchset.Clear ();
  691. if (autoHide || noCopy)
  692. return;
  693. SetSearchSet ();
  694. }
  695. private void SetSearchSet ()
  696. {
  697. if (Source == null) { return; }
  698. // force deep copy
  699. foreach (var item in Source.ToList ()) {
  700. searchset.Add (item);
  701. }
  702. }
  703. private void Search_Changed (object sender, TextChangedEventArgs e)
  704. {
  705. if (source is null) { // Object initialization
  706. return;
  707. }
  708. if (string.IsNullOrEmpty (search.Text) && string.IsNullOrEmpty (e.OldValue)) {
  709. ResetSearchSet ();
  710. } else if (search.Text != e.OldValue) {
  711. if (search.Text.Length < e.OldValue.Length) {
  712. selectedItem = -1;
  713. }
  714. isShow = true;
  715. ResetSearchSet (noCopy: true);
  716. foreach (var item in source.ToList ()) { // Iterate to preserver object type and force deep copy
  717. if (item.ToString ().StartsWith (search.Text, StringComparison.CurrentCultureIgnoreCase)) {
  718. searchset.Add (item);
  719. }
  720. }
  721. }
  722. if (HasFocus) {
  723. ShowList ();
  724. } else if (autoHide) {
  725. isShow = false;
  726. HideList ();
  727. }
  728. }
  729. /// <summary>
  730. /// Show the search list
  731. /// </summary>
  732. ///
  733. /// Consider making public
  734. private void ShowList ()
  735. {
  736. listview.SetSource (searchset);
  737. listview.Clear (); // Ensure list shrinks in Dialog as you type
  738. listview.Height = CalculatetHeight ();
  739. SuperView?.BringSubviewToFront (this);
  740. }
  741. /// <summary>
  742. /// Hide the search list
  743. /// </summary>
  744. ///
  745. /// Consider making public
  746. private void HideList ()
  747. {
  748. if (lastSelectedItem != selectedItem) {
  749. OnOpenSelectedItem ();
  750. }
  751. var rect = listview.BoundsToScreen (listview.Bounds);
  752. Reset (keepSearchText: true);
  753. listview.Clear (rect);
  754. listview.TabStop = false;
  755. SuperView?.SendSubviewToBack (this);
  756. SuperView?.SetNeedsDisplay (rect);
  757. OnCollapsed ();
  758. }
  759. /// <summary>
  760. /// Internal height of dynamic search list
  761. /// </summary>
  762. /// <returns></returns>
  763. private int CalculatetHeight ()
  764. {
  765. if (Bounds.Height == 0)
  766. return 0;
  767. return Math.Min (Math.Max (Bounds.Height - 1, minimumHeight - 1), searchset?.Count > 0 ? searchset.Count : isShow ? Math.Max (Bounds.Height - 1, minimumHeight - 1) : 0);
  768. }
  769. }
  770. }