diff --git a/Example/demo.cs b/Example/demo.cs index 6ffaa21cc..53974b3c0 100644 --- a/Example/demo.cs +++ b/Example/demo.cs @@ -1,5 +1,7 @@ using Terminal.Gui; using System; +using System.Linq; +using System.IO; using Mono.Terminal; using System.Collections.Generic; using System.Diagnostics; @@ -410,6 +412,27 @@ static class Demo { } MessageBox.Query (60, 10, "Selected Animals", result == "" ? "No animals selected" : result, "Ok"); } + + static void ComboBoxDemo () + { + IList items = new List (); + foreach (var dir in new [] { "/etc", @"\windows\System32" }) { + if (Directory.Exists (dir)) { + items = Directory.GetFiles (dir) + .Select (Path.GetFileName) + .Where (x => char.IsLetterOrDigit (x [0])) + .Distinct () + .OrderBy (x => x).ToList (); + } + } + var list = new ComboBox (0, 0, 36, 7, items); + list.Changed += (object sender, ustring text) => { Application.RequestStop (); }; + + var d = new Dialog ("Select source file", 40, 12) { list }; + Application.Run (d); + + MessageBox.Query (60, 10, "Selected file", list.Text.ToString() == "" ? "Nothing selected" : list.Text.ToString(), "Ok"); + } #endregion @@ -540,6 +563,7 @@ static class Demo { new MenuBarItem ("_List Demos", new MenuItem [] { new MenuItem ("Select _Multiple Items", "", () => ListSelectionDemo (true)), new MenuItem ("Select _Single Item", "", () => ListSelectionDemo (false)), + new MenuItem ("Search Single Item", "", ComboBoxDemo) }), new MenuBarItem ("A_ssorted", new MenuItem [] { new MenuItem ("_Show text alignments", "", () => ShowTextAlignments ()), diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs new file mode 100644 index 000000000..627ec6fc7 --- /dev/null +++ b/Terminal.Gui/Views/ComboBox.cs @@ -0,0 +1,227 @@ +// +// 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; + + readonly IList listsource; + IList searchset; + ustring text = ""; + readonly TextField search; + readonly ListView listview; + readonly int height; + readonly int width; + bool autoHide = true; + + /// + /// Public constructor + /// + /// The x coordinate + /// The y coordinate + /// The width + /// The height + /// Auto completetion source + public ComboBox(int x, int y, int w, int h, IList source) + { + listsource = new List(source); + height = h; + width = w; + search = new TextField(x, y, w, ""); + search.Changed += Search_Changed; + + listview = new ListView(new Rect(x, y + 1, w, 0), listsource.ToList()) + { + LayoutStyle = LayoutStyle.Computed, + }; + listview.SelectedChanged += (object sender, ListViewItemEventArgs e) => { + if(searchset.Count > 0) + SetValue (searchset [listview.SelectedItem]); + }; + + Application.Loaded += (object sender, Application.ResizedEventArgs e) => { + // 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 + listview.X = x; + listview.Y = y + 1; + listview.Width = CalculateWidth(); + listview.Height = CalculatetHeight (); + + if (autoHide) + listview.ColorScheme = Colors.Menu; + else + search.ColorScheme = Colors.Menu; + }; + + this.Add(listview); + this.Add(search); + this.SetFocus(search); + } + + /// + 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.SetSource(new List ()); + 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 && 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 currenlty 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 (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 + /// + /// + private int CalculateWidth() + { + return autoHide? Math.Max (1, width - 1) : width; + } + } +} diff --git a/UICatalog/Scenarios/ListsAndCombos.cs b/UICatalog/Scenarios/ListsAndCombos.cs new file mode 100644 index 000000000..f5633e47b --- /dev/null +++ b/UICatalog/Scenarios/ListsAndCombos.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.IO; +using System.Collections.Generic; +using Terminal.Gui; +using NStack; + +namespace UICatalog.Scenarios { + [ScenarioMetadata (Name: "Lists", Description: "Demonstrates list selections")] + [ScenarioCategory ("Controls")] + class ListsAndCombos : Scenario { + + public override void Setup () + { + List items = new List (); + foreach (var dir in new [] { "/etc", @"\windows\System32" }) { + if (Directory.Exists (dir)) { + items = Directory.GetFiles (dir) + .Select (Path.GetFileName) + .Where (x => char.IsLetterOrDigit (x [0])) + .Distinct () + .OrderBy (x => x).ToList (); + } + } + + // ListView + var lbListView = new Label ("Listview") { + ColorScheme = Colors.TopLevel, + X = 0, + Width = 30 + }; + + var listview = new ListView (items) { + X = 0, + Y = Pos.Bottom (lbListView) + 1, + Width = 30 + }; + listview.OpenSelectedItem += (object sender, ListViewItemEventArgs e) => lbListView.Text = items [listview.SelectedItem]; + Win.Add (lbListView, listview); + + // ComboBox + var lbComboBox = new Label ("ComboBox") { + ColorScheme = Colors.TopLevel, + X = Pos.Right (lbListView) + 1, + Width = 30 + }; + + var comboBox = new ComboBox (0, 0, 30, 10, items) { + X = Pos.Right(listview) + 1 , + Y = Pos.Bottom (lbListView) +1, + Width = 30 + }; + comboBox.Changed += (object sender, ustring text) => lbComboBox.Text = text; + Win.Add (lbComboBox, comboBox); + } + } +}