diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 1baf85993..8d60a3b0d 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -53,6 +53,14 @@ namespace Terminal.Gui { /// public event EventHandler SelectedTabChanged; + + /// + /// Event fired when a is clicked. Can be used to cancel navigation, + /// show context menu (e.g. on right click) etc. + /// + public event EventHandler TabClicked; + + /// /// The currently selected member of chosen by the user /// @@ -665,6 +673,22 @@ namespace Terminal.Gui { 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) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && !me.Flags.HasFlag (MouseFlags.Button1TripleClicked)) @@ -689,7 +713,7 @@ namespace Terminal.Gui { return true; } - var hit = ScreenToTab (me.X, me.Y); + if (hit != null) { host.SelectedTab = hit; SetNeedsDisplay (); @@ -738,6 +762,45 @@ namespace Terminal.Gui { } } + /// + /// Raises the event. + /// + /// + protected virtual private void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) + { + TabClicked?.Invoke (this, tabMouseEventArgs); + } + + /// + /// Describes a mouse event over a specific in a . + /// + public class TabMouseEventArgs : EventArgs { + + /// + /// Gets the (if any) that the mouse + /// was over when the occurred. + /// + /// This will be null if the click is after last tab + /// or before first. + public Tab Tab { get; } + + /// + /// Gets the actual mouse event. Use to cancel this event + /// and perform custom behavior (e.g. show a context menu). + /// + public MouseEvent MouseEvent { get; } + + /// + /// Creates a new instance of the class. + /// + /// that the mouse was over when the event occurred. + /// The mouse activity being reported + public TabMouseEventArgs (Tab tab, MouseEvent mouseEvent) + { + Tab = tab; + MouseEvent = mouseEvent; + } + } /// /// A single tab in a diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index a4fe1992e..0bb238d78 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -38,6 +38,8 @@ namespace UICatalog.Scenarios { Height = Dim.Fill (1), }; + tabView.TabClicked += TabView_TabClicked; + tabView.Style.ShowBorder = true; tabView.ApplyStyleChanges (); @@ -63,6 +65,34 @@ namespace UICatalog.Scenarios { 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 () { Open ("", null, $"new {numbeOfNewTabs++}"); @@ -70,7 +100,11 @@ namespace UICatalog.Scenarios { 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) { return; @@ -158,7 +192,11 @@ namespace UICatalog.Scenarios { 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) { return; diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 79da1e3a4..5177b638e 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -760,6 +760,98 @@ namespace Terminal.Gui.ViewTests { └──────────────┘ ", 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 () { var driver = new FakeDriver ();