Browse Source

Merge pull request #2299 from tznind/tabviewclick

Fixes #2298 - Adds TabView.TabClicked event
Tig 2 years ago
parent
commit
3d75be4ea6
3 changed files with 196 additions and 3 deletions
  1. 64 1
      Terminal.Gui/Views/TabView.cs
  2. 40 2
      UICatalog/Scenarios/Notepad.cs
  3. 92 0
      UnitTests/Views/TabViewTests.cs

+ 64 - 1
Terminal.Gui/Views/TabView.cs

@@ -53,6 +53,14 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// </summary>
 		public event EventHandler<TabChangedEventArgs> SelectedTabChanged;
 		public event EventHandler<TabChangedEventArgs> SelectedTabChanged;
 
 
+
+		/// <summary>
+		/// Event fired when a <see cref="TabView.Tab"/> is clicked.  Can be used to cancel navigation,
+		/// show context menu (e.g. on right click) etc.
+		/// </summary>
+		public event EventHandler<TabMouseEventArgs> TabClicked;
+
+
 		/// <summary>
 		/// <summary>
 		/// The currently selected member of <see cref="Tabs"/> chosen by the user
 		/// The currently selected member of <see cref="Tabs"/> chosen by the user
 		/// </summary>
 		/// </summary>
@@ -665,6 +673,22 @@ namespace Terminal.Gui {
 
 
 			public override bool MouseEvent (MouseEvent me)
 			public override bool MouseEvent (MouseEvent me)
 			{
 			{
+				var hit = ScreenToTab (me.X, me.Y);
+
+				bool isClick = me.Flags.HasFlag (MouseFlags.Button1Clicked) ||
+					me.Flags.HasFlag (MouseFlags.Button2Clicked) ||
+					me.Flags.HasFlag (MouseFlags.Button3Clicked);
+
+				if (isClick) {
+					host.OnTabClicked (new TabMouseEventArgs (hit, me));
+
+					// user canceled click
+					if (me.Handled) {
+						return true;
+					}
+				}
+
+
 				if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) &&
 				if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) &&
 				!me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
 				!me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
 				!me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
 				!me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
@@ -689,7 +713,7 @@ namespace Terminal.Gui {
 						return true;
 						return true;
 					}
 					}
 
 
-					var hit = ScreenToTab (me.X, me.Y);
+
 					if (hit != null) {
 					if (hit != null) {
 						host.SelectedTab = hit;
 						host.SelectedTab = hit;
 						SetNeedsDisplay ();
 						SetNeedsDisplay ();
@@ -738,6 +762,45 @@ namespace Terminal.Gui {
 			}
 			}
 		}
 		}
 
 
+		/// <summary>
+		/// Raises the <see cref="TabClicked"/> event.
+		/// </summary>
+		/// <param name="tabMouseEventArgs"></param>
+		protected virtual private void OnTabClicked (TabMouseEventArgs tabMouseEventArgs)
+		{
+			TabClicked?.Invoke (this, tabMouseEventArgs);
+		}
+
+		/// <summary>
+		/// Describes a mouse event over a specific <see cref="TabView.Tab"/> in a <see cref="TabView"/>.
+		/// </summary>
+		public class TabMouseEventArgs : EventArgs {
+
+			/// <summary>
+			/// Gets the <see cref="TabView.Tab"/> (if any) that the mouse
+			/// was over when the <see cref="MouseEvent"/> occurred.
+			/// </summary>
+			/// <remarks>This will be null if the click is after last tab
+			/// or before first.</remarks>
+			public Tab Tab { get; }
+
+			/// <summary>
+			/// Gets the actual mouse event.  Use <see cref="MouseEvent.Handled"/> to cancel this event
+			/// and perform custom behavior (e.g. show a context menu).
+			/// </summary>
+			public MouseEvent MouseEvent { get; }
+
+			/// <summary>
+			/// Creates a new instance of the <see cref="TabMouseEventArgs"/> class.
+			/// </summary>
+			/// <param name="tab"><see cref="TabView.Tab"/> that the mouse was over when the event occurred.</param>
+			/// <param name="mouseEvent">The mouse activity being reported</param>
+			public TabMouseEventArgs (Tab tab, MouseEvent mouseEvent)
+			{
+				Tab = tab;
+				MouseEvent = mouseEvent;
+			}
+		}
 
 
 		/// <summary>
 		/// <summary>
 		/// A single tab in a <see cref="TabView"/>
 		/// A single tab in a <see cref="TabView"/>

+ 40 - 2
UICatalog/Scenarios/Notepad.cs

@@ -38,6 +38,8 @@ namespace UICatalog.Scenarios {
 				Height = Dim.Fill (1),
 				Height = Dim.Fill (1),
 			};
 			};
 
 
+			tabView.TabClicked += TabView_TabClicked;
+
 			tabView.Style.ShowBorder = true;
 			tabView.Style.ShowBorder = true;
 			tabView.ApplyStyleChanges ();
 			tabView.ApplyStyleChanges ();
 
 
@@ -63,6 +65,34 @@ namespace UICatalog.Scenarios {
 			New ();
 			New ();
 		}
 		}
 
 
+		private void TabView_TabClicked (object sender, TabView.TabMouseEventArgs e)
+		{
+			// we are only interested in right clicks
+			if(!e.MouseEvent.Flags.HasFlag(MouseFlags.Button3Clicked)) {
+				return;
+			}
+
+			MenuBarItem items;
+
+			if (e.Tab == null) {
+				items = new MenuBarItem (new MenuItem [] {
+					new MenuItem ($"Open", "", () => Open()),
+				});
+
+			} else {
+				items = new MenuBarItem (new MenuItem [] {
+					new MenuItem ($"Save", "", () => Save(e.Tab)),
+					new MenuItem ($"Close", "", () => Close(e.Tab)),
+				});
+			}
+
+
+			var contextMenu = new ContextMenu (e.MouseEvent.X + 1, e.MouseEvent.Y + 1, items);
+
+			contextMenu.Show ();
+			e.MouseEvent.Handled = true;
+		}
+
 		private void New ()
 		private void New ()
 		{
 		{
 			Open ("", null, $"new {numbeOfNewTabs++}");
 			Open ("", null, $"new {numbeOfNewTabs++}");
@@ -70,7 +100,11 @@ namespace UICatalog.Scenarios {
 
 
 		private void Close ()
 		private void Close ()
 		{
 		{
-			var tab = tabView.SelectedTab as OpenedFile;
+			Close (tabView.SelectedTab);
+		}
+		private void Close (TabView.Tab tabToClose)
+		{
+			var tab = tabToClose as OpenedFile;
 
 
 			if (tab == null) {
 			if (tab == null) {
 				return;
 				return;
@@ -158,7 +192,11 @@ namespace UICatalog.Scenarios {
 
 
 		public void Save ()
 		public void Save ()
 		{
 		{
-			var tab = tabView.SelectedTab as OpenedFile;
+			Save (tabView.SelectedTab);
+		}
+		public void Save (TabView.Tab tabToSave)
+		{
+			var tab = tabToSave as OpenedFile;
 
 
 			if (tab == null) {
 			if (tab == null) {
 				return;
 				return;

+ 92 - 0
UnitTests/Views/TabViewTests.cs

@@ -760,6 +760,98 @@ namespace Terminal.Gui.ViewTests {
 └──────────────┘    ", output);
 └──────────────┘    ", output);
 		}
 		}
 
 
+		[Fact, AutoInitShutdown]
+		public void MouseClick_ChangesTab ()
+		{
+			var tv = GetTabView (out var tab1, out var tab2, false);
+
+			tv.Width = 20;
+			tv.Height = 5;
+
+			tv.LayoutSubviews ();
+
+			tv.Redraw (tv.Bounds);
+
+			var tabRow = tv.Subviews[0];
+			Assert.Equal("TabRowView",tabRow.GetType().Name);
+
+			TestHelpers.AssertDriverContentsAre (@"
+┌────┐              
+│Tab1│Tab2          
+│    └─────────────┐
+│hi                │
+└──────────────────┘
+", output);
+
+			TabView.Tab clicked = null;
+			
+
+			tv.TabClicked += (s,e)=>{
+				clicked = e.Tab;
+			};
+
+			// Waving mouse around does not trigger click
+			for(int i=0;i<100;i++)
+			{
+				tabRow.MouseEvent(new MouseEvent{
+						X = i,
+						Y = 1,
+						Flags = MouseFlags.ReportMousePosition
+				});
+
+				Assert.Null(clicked);
+				Assert.Equal(tab1, tv.SelectedTab);
+			}
+
+			tabRow.MouseEvent(new MouseEvent{
+					X = 3,
+					Y = 1,
+					Flags = MouseFlags.Button1Clicked
+			});
+
+			Assert.Equal(tab1, clicked);
+			Assert.Equal(tab1, tv.SelectedTab);
+
+
+			// Click to tab2
+			tabRow.MouseEvent(new MouseEvent{
+					X = 7,
+					Y = 1,
+					Flags = MouseFlags.Button1Clicked
+			});
+
+			Assert.Equal(tab2, clicked);
+			Assert.Equal(tab2, tv.SelectedTab);
+
+			// cancel navigation
+			tv.TabClicked += (s,e)=>{
+				clicked = e.Tab;
+				e.MouseEvent.Handled = true;
+			};
+
+			tabRow.MouseEvent(new MouseEvent{
+					X = 3,
+					Y = 1,
+					Flags = MouseFlags.Button1Clicked
+			});
+
+			// Tab 1 was clicked but event handler blocked navigation
+			Assert.Equal(tab1, clicked);
+			Assert.Equal(tab2, tv.SelectedTab);
+
+
+			tabRow.MouseEvent (new MouseEvent {
+				X = 10,
+				Y = 1,
+				Flags = MouseFlags.Button1Clicked
+			});
+
+			// Clicking beyond last tab should raise event with null Tab
+			Assert.Null (clicked);
+			Assert.Equal (tab2, tv.SelectedTab);
+
+		}
+
 		private void InitFakeDriver ()
 		private void InitFakeDriver ()
 		{
 		{
 			var driver = new FakeDriver ();
 			var driver = new FakeDriver ();