// // ComboBox.cs: ComboBox control // // Authors: // Ross Ferguson (ross.c.ferguson@btinternet.com) // using System; using System.Linq; using System.Collections.Generic; using NStack; namespace Terminal.Gui { /// /// ComboBox control /// public class ComboBox : View { /// /// 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 Changed; IList listsource; IList searchset; ustring text = ""; TextField search; ListView listview; int x; int y; int height; int width; bool autoHide = true; /// /// Public constructor /// public ComboBox () : base() { search = new TextField ("") { LayoutStyle = LayoutStyle.Computed }; listview = new ListView () { LayoutStyle = LayoutStyle.Computed, CanFocus = true /* why? */ }; Initialize (); } /// /// Public constructor /// /// The x coordinate /// The y coordinate /// The width /// The height /// Auto completion source public ComboBox (int x, int y, int w, int h, IList source) { SetSource (source); this.x = x; this.y = y; height = h; width = w; search = new TextField (x, y, w, ""); listview = new ListView (new Rect (x, y + 1, w, 0), listsource.ToList ()) { LayoutStyle = LayoutStyle.Computed, }; Initialize (); } private void Initialize() { search.Changed += Search_Changed; listview.SelectedChanged += (object sender, ListViewItemEventArgs e) => { if(searchset.Count > 0) SetValue (searchset [listview.SelectedItem]); }; // TODO: LayoutComplete event breaks cursor up/down. Revert to Application.Loaded Application.Loaded += (sender, 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; } } searchset = autoHide ? new List () : listsource; // Needs to be re-applied for LayoutStyle.Computed // If Dim or Pos are null, these are the from the parametrized constructor if (X == null) listview.X = x; if (Y == null) listview.Y = y + 1; else listview.Y = Pos.Bottom (search); if (Width == null) listview.Width = CalculateWidth (); else { width = GetDimAsInt (Width); listview.Width = CalculateWidth (); } if (Height == null) listview.Height = CalculatetHeight (); else { height = GetDimAsInt (Height); listview.Height = CalculatetHeight (); } if (this.Text != null) Search_Changed (search, Text); if (autoHide) listview.ColorScheme = Colors.Menu; else search.ColorScheme = Colors.Menu; }; search.MouseClick += Search_MouseClick; this.Add(listview, search); this.SetFocus(search); } /// /// Set search list source /// public void SetSource(IList source) { listsource = new List (source); } private void Search_MouseClick (object sender, MouseEventEventArgs e) { if (e.MouseEvent.Flags != MouseFlags.Button1Clicked) return; SuperView.SetFocus ((View)sender); } /// public override bool OnEnter () { if (!search.HasFocus) this.SetFocus (search); search.CursorPosition = search.Text.Length; 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) { if (listview.Source.Count == 0 || searchset.Count == 0) { text = ""; return true; } SetValue( searchset [listview.SelectedItem]); search.CursorPosition = search.Text.Length; Search_Changed (search, search.Text); Changed?.Invoke (this, text); searchset.Clear(); listview.Clear (); listview.Height = 0; this.SetFocus(search); return true; } if (e.Key == Key.CursorDown && search.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) { // jump to list this.SetFocus (listview); SetValue (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 = ""; Changed?.Invoke (this, search.Text); 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.Changed -= Search_Changed; this.text = search.Text = text; search.CursorPosition = 0; search.Changed += Search_Changed; } /// /// Reset to full original list /// private void Reset() { search.Text = text = ""; Changed?.Invoke (this, search.Text); searchset = autoHide ? new List () : listsource; listview.SetSource(searchset.ToList()); listview.Height = CalculatetHeight (); this.SetFocus(search); } private void Search_Changed (object sender, ustring text) { if (listsource == null) // Object initialization return; if (string.IsNullOrEmpty (search.Text.ToString())) searchset = autoHide ? new List () : listsource; else searchset = listsource.Where (x => x.StartsWith (search.Text.ToString (), StringComparison.CurrentCultureIgnoreCase)).ToList (); listview.SetSource (searchset.ToList ()); listview.Height = CalculatetHeight (); listview.Redraw (new Rect (0, 0, width, height)); // for any view behind this this.SuperView?.BringSubviewToFront (this); } /// /// Internal height of dynamic search list /// /// private int CalculatetHeight () { return Math.Min (height, searchset.Count); } /// /// Internal width of search list /// /// private int CalculateWidth () { return autoHide ? Math.Max (1, width - 1) : width; } /// /// Get DimAbsolute as integer value /// /// /// private int GetDimAsInt(Dim dim) { if (!(dim is Dim.DimAbsolute)) throw new ArgumentException ("Dim must be an absolute value"); // Anchor in the case of DimAbsolute returns absolute value. No documentation on what Anchor() does so not sure if this will change in the future. // // TODO: Does Dim need:- // public static implicit operator int (Dim d) // return dim.Anchor (0); } } }