// // ComboBox.cs: ComboBox control // // Authors: // Ross Ferguson (ross.c.ferguson@btinternet.com) // using System; using System.Collections; using System.Collections.Generic; using System.Linq; using NStack; namespace Terminal.Gui { /// /// ComboBox control /// public class ComboBox : View { IListDataSource source; /// /// Gets or sets the backing this , enabling custom rendering. /// /// The source. /// /// Use to set a new source. /// public IListDataSource Source { get => source; set { source = value; Search_Changed (""); SetNeedsDisplay (); } } /// /// Sets the source of the to an . /// /// An object implementing the IList interface. /// /// Use the property to set a new source and use custome rendering. /// public void SetSource (IList source) { if (source == null) Source = null; else { Source = MakeWrapper (source); } } /// /// Changed event, raised when the selection has been confirmed. /// /// /// Client code can hook up to this event, it is /// raised when the selection has been confirmed. /// public event EventHandler SelectedItemChanged; IList searchset; ustring text = ""; readonly TextField search; readonly ListView listview; int height; int width; bool autoHide = true; /// /// Public constructor /// public ComboBox () : base() { search = new TextField (""); listview = new ListView () { LayoutStyle = LayoutStyle.Computed, CanFocus = true }; Initialize (); } /// /// Public constructor /// /// /// public ComboBox (Rect rect, IList source) : base (rect) { this.height = rect.Height; this.width = rect.Width; search = new TextField ("") { Width = width }; listview = new ListView (rect, source) { LayoutStyle = LayoutStyle.Computed }; Initialize (); SetSource (source); } static IListDataSource MakeWrapper (IList source) { return new ListWrapper (source); } private void Initialize() { ColorScheme = Colors.Base; search.TextChanged += Search_Changed; listview.OpenSelectedItem += (ListViewItemEventArgs a) => Selected(); // On resize LayoutComplete += (LayoutEventArgs a) => { search.Width = Bounds.Width; listview.Width = autoHide ? Bounds.Width - 1 : Bounds.Width; listview.Height = CalculatetHeight (); }; listview.SelectedItemChanged += (ListViewItemEventArgs e) => { if(searchset.Count > 0) SetValue ((string)searchset [listview.SelectedItem]); }; Application.Loaded += (Application.ResizedEventArgs a) => { // Determine if this view is hosted inside a dialog for (View view = this.SuperView; view != null; view = view.SuperView) { if (view is Dialog) { autoHide = false; break; } } ResetSearchSet (); ColorScheme = autoHide ? Colors.Base : ColorScheme = null; listview.Y = Pos.Bottom (search); if (Width != null && width == 0) // new ComboBox() { Width = width = Bounds.Width; search.Width = width; listview.Width = CalculateWidth (); if (Height != null && height == 0) // new ComboBox() { Height = height = Bounds.Height; listview.Height = CalculatetHeight (); SetNeedsLayout (); if (this.Text != null) Search_Changed (Text); if (autoHide) listview.ColorScheme = Colors.Menu; else search.ColorScheme = Colors.Menu; }; search.MouseClick += Search_MouseClick; this.Add(listview, search); this.SetFocus(search); } private void Search_MouseClick (MouseEventArgs e) { if (e.MouseEvent.Flags != MouseFlags.Button1Clicked) return; SuperView.SetFocus (search); } /// public override bool OnEnter () { if (!search.HasFocus) this.SetFocus (search); search.CursorPosition = search.Text.Length; return true; } /// /// Invokes the SelectedChanged event if it is defined. /// /// public virtual bool OnSelectedChanged () { // Note: Cannot rely on "listview.SelectedItem != lastSelectedItem" because the list is dynamic. // So we cannot optimize. Ie: Don't call if not changed SelectedItemChanged?.Invoke (this, search.Text); return true; } /// public override bool ProcessKey(KeyEvent e) { if (e.Key == Key.Tab) { base.ProcessKey(e); return false; // allow tab-out to next control } if (e.Key == Key.Enter && listview.HasFocus) { Selected (); return true; } if (e.Key == Key.CursorDown && search.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) { // jump to list this.SetFocus (listview); SetValue ((string)searchset [listview.SelectedItem]); return true; } if (e.Key == Key.CursorUp && search.HasFocus) // stop odd behavior on KeyUp when search has focus return true; if (e.Key == Key.CursorUp && listview.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) // jump back to search { search.CursorPosition = search.Text.Length; this.SetFocus (search); return true; } if (e.Key == Key.Esc) { this.SetFocus (search); search.Text = text = ""; OnSelectedChanged (); return true; } // Unix emulation if (e.Key == Key.ControlU) { Reset(); return true; } return base.ProcessKey(e); } /// /// The currently selected list item /// public ustring Text { get { return text; } set { search.Text = text = value; } } private void SetValue(ustring text) { search.TextChanged -= Search_Changed; this.text = search.Text = text; search.CursorPosition = 0; search.TextChanged += Search_Changed; } private void Selected() { if (listview.Source.Count == 0 || searchset.Count == 0) { text = ""; return; } SetValue ((string)searchset [listview.SelectedItem]); search.CursorPosition = search.Text.Length; Search_Changed (search.Text); Reset (keepSearchText: true); } /// /// Reset to full original list /// private void Reset(bool keepSearchText = false) { if(!keepSearchText) search.Text = text = ""; OnSelectedChanged (); ResetSearchSet (); listview.SetSource(searchset); listview.Height = CalculatetHeight (); this.SetFocus(search); } private void ResetSearchSet() { if (autoHide) { if (searchset == null) searchset = new List (); else searchset.Clear (); } else searchset = source.ToList (); } private void Search_Changed (ustring text) { if (source == null) // Object initialization return; if (string.IsNullOrEmpty (search.Text.ToString ())) ResetSearchSet (); else searchset = source.ToList().Cast().Where (x => x.StartsWith (search.Text.ToString (), StringComparison.CurrentCultureIgnoreCase)).ToList(); listview.SetSource (searchset); listview.Height = CalculatetHeight (); listview.Redraw (new Rect (0, 0, width, height)); // for any view behind this this.SuperView?.BringSubviewToFront (this); } /// /// Internal width of search list /// /// private int CalculateWidth () { return autoHide ? Math.Max (1, width - 1) : width; } /// /// Internal height of dynamic search list /// /// private int CalculatetHeight () { var h = (Height is Dim.DimAbsolute) ? height : Bounds.Height; return Math.Min (Math.Max (0, h - 1), searchset?.Count ?? 0); } } }