瀏覽代碼

Fixes #820. Added HideDropdownListOnClick property. (#1983)

* Added HideDropdownListOnClick property.

* Invoking OpenSelectedItem on click.

* Added one more constructor and more unit tests.

* Added the ComboListView derived class.

* Added more features to ComboBox.

* Removes code not featured yet.

* Added HideDropdownListOnClick property to scenario.

* Ensures SetSource having the same behavior as the ListWrapper constructor.
BDisp 2 年之前
父節點
當前提交
b21f203292

+ 228 - 11
Terminal.Gui/Views/ComboBox.cs

@@ -16,6 +16,161 @@ namespace Terminal.Gui {
 	/// </summary>
 	public class ComboBox : View {
 
+		private class ComboListView : ListView {
+			private int highlighted = -1;
+			private bool isFocusing;
+			private ComboBox container;
+			private bool hideDropdownListOnClick;
+
+			public ComboListView (ComboBox container, bool hideDropdownListOnClick)
+			{
+				Initialize (container, hideDropdownListOnClick);
+			}
+
+			public ComboListView (ComboBox container, Rect rect, IList source, bool hideDropdownListOnClick) : base (rect, source)
+			{
+				Initialize (container, hideDropdownListOnClick);
+			}
+
+			public ComboListView (ComboBox container, IList source, bool hideDropdownListOnClick) : base (source)
+			{
+				Initialize (container, hideDropdownListOnClick);
+			}
+
+			private void Initialize (ComboBox container, bool hideDropdownListOnClick)
+			{
+				if (container == null)
+					throw new ArgumentNullException ("ComboBox container cannot be null.", nameof (container));
+
+				this.container = container;
+				HideDropdownListOnClick = hideDropdownListOnClick;
+			}
+
+			public bool HideDropdownListOnClick {
+				get => hideDropdownListOnClick;
+				set => hideDropdownListOnClick = WantContinuousButtonPressed = value;
+			}
+
+			public override bool MouseEvent (MouseEvent me)
+			{
+				var res = false;
+				var isMousePositionValid = IsMousePositionValid (me);
+
+				if (isMousePositionValid) {
+					res = base.MouseEvent (me);
+				}
+
+				if (HideDropdownListOnClick && me.Flags == MouseFlags.Button1Clicked) {
+					if (!isMousePositionValid && !isFocusing) {
+						container.isShow = false;
+						container.HideList ();
+					} else if (isMousePositionValid) {
+						OnOpenSelectedItem ();
+					} else {
+						isFocusing = false;
+					}
+					return true;
+				} else if (me.Flags == MouseFlags.ReportMousePosition && HideDropdownListOnClick) {
+					if (isMousePositionValid) {
+						highlighted = Math.Min (TopItem + me.Y, Source.Count);
+						SetNeedsDisplay ();
+					}
+					isFocusing = false;
+					return true;
+				}
+
+				return res;
+			}
+
+			private bool IsMousePositionValid (MouseEvent me)
+			{
+				if (me.X >= 0 && me.X < Frame.Width && me.Y >= 0 && me.Y < Frame.Height) {
+					return true;
+				}
+				return false;
+			}
+
+			public override void Redraw (Rect bounds)
+			{
+				var current = ColorScheme.Focus;
+				Driver.SetAttribute (current);
+				Move (0, 0);
+				var f = Frame;
+				var item = TopItem;
+				bool focused = HasFocus;
+				int col = AllowsMarking ? 2 : 0;
+				int start = LeftItem;
+
+				for (int row = 0; row < f.Height; row++, item++) {
+					bool isSelected = item == container.SelectedItem;
+					bool isHighlighted = hideDropdownListOnClick && item == highlighted;
+
+					Attribute newcolor;
+					if (isHighlighted || (isSelected && !hideDropdownListOnClick)) {
+						newcolor = focused ? ColorScheme.Focus : ColorScheme.HotNormal;
+					} else if (isSelected && hideDropdownListOnClick) {
+						newcolor = focused ? ColorScheme.HotFocus : ColorScheme.HotNormal;
+					} else {
+						newcolor = focused ? GetNormalColor () : GetNormalColor ();
+					}
+
+					if (newcolor != current) {
+						Driver.SetAttribute (newcolor);
+						current = newcolor;
+					}
+
+					Move (0, row);
+					if (Source == null || item >= Source.Count) {
+						for (int c = 0; c < f.Width; c++)
+							Driver.AddRune (' ');
+					} else {
+						var rowEventArgs = new ListViewRowEventArgs (item);
+						OnRowRender (rowEventArgs);
+						if (rowEventArgs.RowAttribute != null && current != rowEventArgs.RowAttribute) {
+							current = (Attribute)rowEventArgs.RowAttribute;
+							Driver.SetAttribute (current);
+						}
+						if (AllowsMarking) {
+							Driver.AddRune (Source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
+							Driver.AddRune (' ');
+						}
+						Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start);
+					}
+				}
+			}
+
+			public override bool OnEnter (View view)
+			{
+				if (hideDropdownListOnClick) {
+					isFocusing = true;
+					highlighted = container.SelectedItem;
+					Application.GrabMouse (this);
+				}
+
+				return base.OnEnter (view);
+			}
+
+			public override bool OnLeave (View view)
+			{
+				if (hideDropdownListOnClick) {
+					isFocusing = false;
+					highlighted = container.SelectedItem;
+					Application.UngrabMouse ();
+				}
+
+				return base.OnLeave (view);
+			}
+
+			public override bool OnSelectedChanged ()
+			{
+				var res = base.OnSelectedChanged ();
+
+				highlighted = SelectedItem;
+
+				return res;
+			}
+		}
+
 		IListDataSource source;
 		/// <summary>
 		/// Gets or sets the <see cref="IListDataSource"/> backing this <see cref="ComboBox"/>, enabling custom rendering.
@@ -61,6 +216,16 @@ namespace Terminal.Gui {
 		/// </summary>
 		public event Action<ListViewItemEventArgs> SelectedItemChanged;
 
+		/// <summary>
+		/// This event is raised when the drop-down list is expanded.
+		/// </summary>
+		public event Action Expanded;
+
+		/// <summary>
+		/// This event is raised when the drop-down list is collapsed.
+		/// </summary>
+		public event Action Collapsed;
+
 		/// <summary>
 		/// This event is raised when the user Double Clicks on an item or presses ENTER to open the selected item.
 		/// </summary>
@@ -69,7 +234,7 @@ namespace Terminal.Gui {
 		readonly IList searchset = new List<object> ();
 		ustring text = "";
 		readonly TextField search;
-		readonly ListView listview;
+		readonly ComboListView listview;
 		bool autoHide = true;
 		int minimumHeight = 2;
 
@@ -87,7 +252,7 @@ namespace Terminal.Gui {
 		public ComboBox (ustring text) : base ()
 		{
 			search = new TextField ("");
-			listview = new ListView () { LayoutStyle = LayoutStyle.Computed, CanFocus = true, TabStop = false };
+			listview = new ComboListView (this, HideDropdownListOnClick) { LayoutStyle = LayoutStyle.Computed, CanFocus = true, TabStop = false };
 
 			Initialize ();
 			Text = text;
@@ -101,7 +266,20 @@ namespace Terminal.Gui {
 		public ComboBox (Rect rect, IList source) : base (rect)
 		{
 			search = new TextField ("") { Width = rect.Width };
-			listview = new ListView (rect, source) { LayoutStyle = LayoutStyle.Computed, ColorScheme = Colors.Base };
+			listview = new ComboListView (this, rect, source, HideDropdownListOnClick) { LayoutStyle = LayoutStyle.Computed, ColorScheme = Colors.Base };
+
+			Initialize ();
+			SetSource (source);
+		}
+
+		/// <summary>
+		/// Initialize with the source.
+		/// </summary>
+		/// <param name="source">The source.</param>
+		public ComboBox (IList source) : this (string.Empty)
+		{
+			search = new TextField ("");
+			listview = new ComboListView (this, source, HideDropdownListOnClick) { LayoutStyle = LayoutStyle.Computed, ColorScheme = Colors.Base };
 
 			Initialize ();
 			SetSource (source);
@@ -133,7 +311,7 @@ namespace Terminal.Gui {
 
 			listview.SelectedItemChanged += (ListViewItemEventArgs e) => {
 
-				if (searchset.Count > 0) {
+				if (!HideDropdownListOnClick && searchset.Count > 0) {
 					SetValue (searchset [listview.SelectedItem]);
 				}
 			};
@@ -182,6 +360,8 @@ namespace Terminal.Gui {
 
 		private bool isShow = false;
 		private int selectedItem = -1;
+		private int lastSelectedItem = -1;
+		private bool hideDropdownListOnClick;
 
 		/// <summary>
 		/// Gets the index of the currently selected item in the <see cref="Source"/>
@@ -193,7 +373,7 @@ namespace Terminal.Gui {
 				if (selectedItem != value && (value == -1
 					|| (source != null && value > -1 && value < source.Count))) {
 
-					selectedItem = value;
+					selectedItem = lastSelectedItem = value;
 					if (selectedItem != -1) {
 						SetValue (source.ToList () [selectedItem].ToString (), true);
 					} else {
@@ -236,6 +416,14 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Gets or sets if the drop-down list can be hide with a button click event.
+		/// </summary>
+		public bool HideDropdownListOnClick {
+			get => hideDropdownListOnClick;
+			set => hideDropdownListOnClick = listview.HideDropdownListOnClick = value;
+		}
+
 		///<inheritdoc/>
 		public override bool MouseEvent (MouseEvent me)
 		{
@@ -268,10 +456,25 @@ namespace Terminal.Gui {
 		private void FocusSelectedItem ()
 		{
 			listview.SelectedItem = SelectedItem > -1 ? SelectedItem : 0;
-			if (SelectedItem > -1) {
-				listview.TabStop = true;
-				listview.SetFocus ();
-			}
+			listview.TabStop = true;
+			listview.SetFocus ();
+			OnExpanded ();
+		}
+
+		/// <summary>
+		/// Virtual method which invokes the <see cref="Expanded"/> event.
+		/// </summary>
+		public virtual void OnExpanded ()
+		{
+			Expanded?.Invoke ();
+		}
+
+		/// <summary>
+		/// Virtual method which invokes the <see cref="Collapsed"/> event.
+		/// </summary>
+		public virtual void OnCollapsed ()
+		{
+			Collapsed?.Invoke ();
 		}
 
 		///<inheritdoc/>
@@ -324,6 +527,7 @@ namespace Terminal.Gui {
 		public virtual bool OnOpenSelectedItem ()
 		{
 			var value = search.Text;
+			lastSelectedItem = SelectedItem;
 			OpenSelectedItem?.Invoke (new ListViewItemEventArgs (SelectedItem, value));
 
 			return true;
@@ -338,6 +542,7 @@ namespace Terminal.Gui {
 				return;
 			}
 
+			Driver.SetAttribute (ColorScheme.Focus);
 			Move (Bounds.Right - 1, 0);
 			Driver.AddRune (Driver.DownArrow);
 		}
@@ -362,8 +567,16 @@ namespace Terminal.Gui {
 		bool CancelSelected ()
 		{
 			search.SetFocus ();
-			search.Text = text = "";
-			OnSelectedChanged ();
+			if (ReadOnly || HideDropdownListOnClick) {
+				SelectedItem = lastSelectedItem;
+				if (SelectedItem > -1 && listview.Source?.Count > 0) {
+					search.Text = text = listview.Source.ToList () [SelectedItem].ToString ();
+				}
+			} else if (!ReadOnly) {
+				search.Text = text = "";
+				selectedItem = lastSelectedItem;
+				OnSelectedChanged ();
+			}
 			Collapse ();
 			return true;
 		}
@@ -635,12 +848,16 @@ namespace Terminal.Gui {
 		/// Consider making public
 		private void HideList ()
 		{
+			if (lastSelectedItem != selectedItem) {
+				OnOpenSelectedItem ();
+			}
 			var rect = listview.ViewToScreen (listview.Bounds);
 			Reset (SelectedItem > -1);
 			listview.Clear (rect);
 			listview.TabStop = false;
 			SuperView?.SendSubviewToBack (this);
 			SuperView?.SetNeedsDisplay (rect);
+			OnCollapsed ();
 		}
 
 		/// <summary>

+ 4 - 4
Terminal.Gui/Views/ListView.cs

@@ -140,7 +140,7 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public void SetSource (IList source)
 		{
-			if (source == null)
+			if (source == null && (Source == null || !(Source is ListWrapper)))
 				Source = null;
 			else {
 				Source = MakeWrapper (source);
@@ -157,7 +157,7 @@ namespace Terminal.Gui {
 		public Task SetSourceAsync (IList source)
 		{
 			return Task.Factory.StartNew (() => {
-				if (source == null)
+				if (source == null && (Source == null || !(Source is ListWrapper)))
 					Source = null;
 				else
 					Source = MakeWrapper (source);
@@ -827,7 +827,7 @@ namespace Terminal.Gui {
 
 		int GetMaxLengthItem ()
 		{
-			if (src?.Count == 0) {
+			if (src == null || src?.Count == 0) {
 				return 0;
 			}
 
@@ -883,7 +883,7 @@ namespace Terminal.Gui {
 		public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width, int start = 0)
 		{
 			container.Move (col, line);
-			var t = src [item];
+			var t = src? [item];
 			if (t == null) {
 				RenderUstr (driver, ustring.Make (""), col, line, width);
 			} else {

+ 2 - 1
UICatalog/Scenarios/ComboBoxIteration.cs

@@ -33,7 +33,8 @@ namespace UICatalog.Scenarios {
 				X = Pos.Right (listview) + 1,
 				Y = Pos.Bottom (lbListView) + 1,
 				Height = Dim.Fill (2),
-				Width = Dim.Percent (40)
+				Width = Dim.Percent (40),
+				HideDropdownListOnClick = true
 			};
 			comboBox.SetSource (items);
 

+ 695 - 0
UnitTests/ComboBoxTests.cs

@@ -21,18 +21,42 @@ namespace Terminal.Gui.Views {
 			Assert.Null (cb.Source);
 			Assert.False (cb.AutoSize);
 			Assert.Equal (new Rect (0, 0, 0, 2), cb.Frame);
+			Assert.Equal (-1, cb.SelectedItem);
 
 			cb = new ComboBox ("Test");
 			Assert.Equal ("Test", cb.Text);
 			Assert.Null (cb.Source);
 			Assert.False (cb.AutoSize);
 			Assert.Equal (new Rect (0, 0, 0, 2), cb.Frame);
+			Assert.Equal (-1, cb.SelectedItem);
 
 			cb = new ComboBox (new Rect (1, 2, 10, 20), new List<string> () { "One", "Two", "Three" });
 			Assert.Equal (string.Empty, cb.Text);
 			Assert.NotNull (cb.Source);
 			Assert.False (cb.AutoSize);
 			Assert.Equal (new Rect (1, 2, 10, 20), cb.Frame);
+			Assert.Equal (-1, cb.SelectedItem);
+
+			cb = new ComboBox (new List<string> () { "One", "Two", "Three" });
+			Assert.Equal (string.Empty, cb.Text);
+			Assert.NotNull (cb.Source);
+			Assert.False (cb.AutoSize);
+			Assert.Equal (new Rect (0, 0, 0, 2), cb.Frame);
+			Assert.Equal (-1, cb.SelectedItem);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void Constructor_With_Source_Initialize_With_The_Passed_SelectedItem ()
+		{
+			var cb = new ComboBox (new List<string> () { "One", "Two", "Three" }) {
+				SelectedItem = 1
+			};
+			Assert.Equal ("Two", cb.Text);
+			Assert.NotNull (cb.Source);
+			Assert.False (cb.AutoSize);
+			Assert.Equal (new Rect (0, 0, 0, 2), cb.Frame);
+			Assert.Equal (1, cb.SelectedItem);
 		}
 
 		[Fact]
@@ -240,5 +264,676 @@ Three
 			Assert.Equal (-1, cb.SelectedItem);
 			Assert.Equal ("", cb.Text);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void HideDropdownListOnClick_Gets_Sets ()
+		{
+			var selected = "";
+			var cb = new ComboBox {
+				Height = 4,
+				Width = 5
+			};
+			cb.SetSource (new List<string> { "One", "Two", "Three" });
+			cb.OpenSelectedItem += (e) => selected = e.Value.ToString ();
+			Application.Top.Add (cb);
+			Application.Begin (Application.Top);
+
+			Assert.False (cb.HideDropdownListOnClick);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = 0,
+				Y = 1,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = 0,
+				Y = 1,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+
+			cb.HideDropdownListOnClick = true;
+
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = 0,
+				Y = 2,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("Three", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = 0,
+				Y = 2,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("Three", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("Three", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = 0,
+				Y = 0,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("One", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void HideDropdownListOnClick_True_OpenSelectedItem_With_Mouse_And_Key_And_Mouse ()
+		{
+			var selected = "";
+			var cb = new ComboBox {
+				Height = 4,
+				Width = 5,
+				HideDropdownListOnClick = true
+			};
+			cb.SetSource (new List<string> { "One", "Two", "Three" });
+			cb.OpenSelectedItem += (e) => selected = e.Value.ToString ();
+			Application.Top.Add (cb);
+			Application.Begin (Application.Top);
+
+			Assert.True (cb.HideDropdownListOnClick);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void HideDropdownListOnClick_True_OpenSelectedItem_With_Mouse_And_Key_CursorDown_And_Esc ()
+		{
+			var selected = "";
+			var cb = new ComboBox {
+				Height = 4,
+				Width = 5,
+				HideDropdownListOnClick = true
+			};
+			cb.SetSource (new List<string> { "One", "Two", "Three" });
+			cb.OpenSelectedItem += (e) => selected = e.Value.ToString ();
+			Application.Top.Add (cb);
+			Application.Begin (Application.Top);
+
+			Assert.True (cb.HideDropdownListOnClick);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.Esc, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void HideDropdownListOnClick_False_OpenSelectedItem_With_Mouse_And_Key_CursorDown_And_Esc ()
+		{
+			var selected = "";
+			var cb = new ComboBox {
+				Height = 4,
+				Width = 5,
+				HideDropdownListOnClick = false
+			};
+			cb.SetSource (new List<string> { "One", "Two", "Three" });
+			cb.OpenSelectedItem += (e) => selected = e.Value.ToString ();
+			Application.Top.Add (cb);
+			Application.Begin (Application.Top);
+
+			Assert.False (cb.HideDropdownListOnClick);
+			Assert.False (cb.ReadOnly);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.Esc, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void HideDropdownListOnClick_False_ReadOnly_True_OpenSelectedItem_With_Mouse_And_Key_CursorDown_And_Esc ()
+		{
+			var selected = "";
+			var cb = new ComboBox {
+				Height = 4,
+				Width = 5,
+				HideDropdownListOnClick = false,
+				ReadOnly = true
+			};
+			cb.SetSource (new List<string> { "One", "Two", "Three" });
+			cb.OpenSelectedItem += (e) => selected = e.Value.ToString ();
+			Application.Top.Add (cb);
+			Application.Begin (Application.Top);
+
+			Assert.False (cb.HideDropdownListOnClick);
+			Assert.True (cb.ReadOnly);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.Esc, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void HideDropdownListOnClick_True_OpenSelectedItem_With_Mouse_And_Key_F4 ()
+		{
+			var selected = "";
+			var cb = new ComboBox {
+				Height = 4,
+				Width = 5,
+				HideDropdownListOnClick = true
+			};
+			cb.SetSource (new List<string> { "One", "Two", "Three" });
+			cb.OpenSelectedItem += (e) => selected = e.Value.ToString ();
+			Application.Top.Add (cb);
+			Application.Begin (Application.Top);
+
+			Assert.True (cb.HideDropdownListOnClick);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.Equal ("", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void HideDropdownListOnClick_False_OpenSelectedItem_With_Mouse_And_Key_F4 ()
+		{
+			var selected = "";
+			var cb = new ComboBox {
+				Height = 4,
+				Width = 5,
+				HideDropdownListOnClick = false
+			};
+			cb.SetSource (new List<string> { "One", "Two", "Three" });
+			cb.OpenSelectedItem += (e) => selected = e.Value.ToString ();
+			Application.Top.Add (cb);
+			Application.Begin (Application.Top);
+
+			Assert.False (cb.HideDropdownListOnClick);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.Equal ("Two", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void HideDropdownListOnClick_True_Colapse_On_Click_Outside_Frame ()
+		{
+			var selected = "";
+			var cb = new ComboBox {
+				Height = 4,
+				Width = 5,
+				HideDropdownListOnClick = true
+			};
+			cb.SetSource (new List<string> { "One", "Two", "Three" });
+			cb.OpenSelectedItem += (e) => selected = e.Value.ToString ();
+			Application.Top.Add (cb);
+			Application.Begin (Application.Top);
+
+			Assert.True (cb.HideDropdownListOnClick);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = -1,
+				Y = 0,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = 0,
+				Y = -1,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = cb.Frame.Width,
+				Y = 0,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+			Assert.True (cb.Subviews [1].MouseEvent (new MouseEvent {
+				X = 0,
+				Y = cb.Frame.Height,
+				Flags = MouseFlags.Button1Clicked
+			}));
+			Assert.Equal ("", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void HideDropdownListOnClick_True_Highlight_Current_Item ()
+		{
+			var selected = "";
+			var cb = new ComboBox {
+				Width = 6,
+				Height = 4,
+				HideDropdownListOnClick = true,
+			};
+			cb.SetSource (new List<string> { "One", "Two", "Three" });
+			cb.OpenSelectedItem += (e) => selected = e.Value.ToString ();
+			Application.Top.Add (cb);
+			Application.Begin (Application.Top);
+
+			Assert.True (cb.HideDropdownListOnClick);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.MouseEvent (new MouseEvent {
+				X = cb.Bounds.Right - 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+			cb.Redraw (cb.Bounds);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+     ▼
+One   
+Two   
+Three ", output);
+
+			var attributes = new Attribute [] {
+				// 0
+				cb.Subviews [0].ColorScheme.Focus,
+				// 1
+				cb.Subviews [1].ColorScheme.HotFocus,
+				// 2
+				cb.Subviews [1].GetNormalColor ()
+			};
+
+			GraphViewTests.AssertDriverColorsAre (@"
+000000
+00000
+22222
+22222", attributes);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+			cb.Redraw (cb.Bounds);
+			GraphViewTests.AssertDriverColorsAre (@"
+000000
+22222
+00000
+22222", attributes);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal ("", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+			cb.Redraw (cb.Bounds);
+			GraphViewTests.AssertDriverColorsAre (@"
+000000
+22222
+22222
+00000", attributes);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.Equal ("Three", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.Equal ("Three", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+			cb.Redraw (cb.Bounds);
+			GraphViewTests.AssertDriverColorsAre (@"
+000000
+22222
+22222
+00000", attributes);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal ("Three", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+			cb.Redraw (cb.Bounds);
+			GraphViewTests.AssertDriverColorsAre (@"
+000000
+22222
+00000
+11111", attributes);
+
+			Assert.True (cb.Subviews [1].ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal ("Three", selected);
+			Assert.True (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+			cb.Redraw (cb.Bounds);
+			GraphViewTests.AssertDriverColorsAre (@"
+000000
+00000
+22222
+11111", attributes);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.Equal ("Three", selected);
+			Assert.False (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Expanded_Collapsed_Events ()
+		{
+			var cb = new ComboBox {
+				Height = 4,
+				Width = 5
+			};
+			var list = new List<string> { "One", "Two", "Three" };
+
+			cb.Expanded += () => cb.SetSource (list);
+			cb.Collapsed += () => cb.Source = null;
+
+			Application.Top.Add (cb);
+			Application.Begin (Application.Top);
+
+			Assert.Null (cb.Source);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.NotNull (cb.Source);
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.Null (cb.Source);
+			Assert.False (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+		}
 	}
 }

+ 20 - 0
UnitTests/ListViewTests.cs

@@ -431,5 +431,25 @@ namespace Terminal.Gui.Views {
 │Line9     │
 └──────────┘", output);
 		}
+
+		[Fact]
+		public void SetSource_Preserves_ListWrapper_Instance_If_Not_Null ()
+		{
+			var lv = new ListView (new List<string> { "One", "Two" });
+
+			Assert.NotNull (lv.Source);
+
+			lv.SetSource (null);
+			Assert.NotNull (lv.Source);
+
+			lv.Source = null;
+			Assert.Null (lv.Source);
+
+			lv = new ListView (new List<string> { "One", "Two" });
+			Assert.NotNull (lv.Source);
+
+			lv.SetSourceAsync (null);
+			Assert.NotNull (lv.Source);
+		}
 	}
 }