From 553cd6f66f1417318edac6a51ea34027e227548c Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 20 Jan 2023 19:30:36 +0000 Subject: [PATCH 1/3] Adds TabView.TabClicked event --- Terminal.Gui/Views/TabView.cs | 63 +++++++++++++++++++++++++++++++++- UICatalog/Scenarios/Notepad.cs | 29 ++++++++++++++-- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 1baf85993..1bac1da3c 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 (hit != null && 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,43 @@ 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 that the mouse was over when the + /// occurred. + /// + 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..5e2fde4d8 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,19 @@ namespace UICatalog.Scenarios { New (); } + private void TabView_TabClicked (object sender, TabView.TabMouseEventArgs e) + { + var contextMenu = new ContextMenu (e.MouseEvent.X + 1, e.MouseEvent.Y + 1, + new MenuBarItem (new MenuItem [] { + new MenuItem ($"Save", "", () => Save(e.Tab)), + new MenuItem ($"Close", "", () => Close(e.Tab)), + }) + ); + + contextMenu.Show (); + e.MouseEvent.Handled = true; + } + private void New () { Open ("", null, $"new {numbeOfNewTabs++}"); @@ -70,7 +85,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; @@ -156,9 +175,13 @@ 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) { return; From 8b8bb2ab608ca337cc994c94cce7e8f21f4d9f6f Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 20 Jan 2023 20:05:53 +0000 Subject: [PATCH 2/3] Test for TabClicked event --- UnitTests/Views/TabViewTests.cs | 81 +++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 79da1e3a4..a177d251c 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -760,6 +760,87 @@ 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); + + } + private void InitFakeDriver () { var driver = new FakeDriver (); From ab398b93cbf949f248b72a8d94672e0b13e1bf92 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 21 Jan 2023 08:31:17 +0000 Subject: [PATCH 3/3] Make OnTabClicked fire for clicks in empty space and fix Notepad to only show context menu on right click. --- Terminal.Gui/Views/TabView.cs | 8 +++++--- UICatalog/Scenarios/Notepad.cs | 27 +++++++++++++++++++++------ UnitTests/Views/TabViewTests.cs | 11 +++++++++++ 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 1bac1da3c..8d60a3b0d 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -679,7 +679,7 @@ namespace Terminal.Gui { me.Flags.HasFlag (MouseFlags.Button2Clicked) || me.Flags.HasFlag (MouseFlags.Button3Clicked); - if (hit != null && isClick) { + if (isClick) { host.OnTabClicked (new TabMouseEventArgs (hit, me)); // user canceled click @@ -777,9 +777,11 @@ namespace Terminal.Gui { public class TabMouseEventArgs : EventArgs { /// - /// Gets the that the mouse was over when the - /// occurred. + /// 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; } /// diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index 5e2fde4d8..0bb238d78 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -67,12 +67,27 @@ namespace UICatalog.Scenarios { private void TabView_TabClicked (object sender, TabView.TabMouseEventArgs e) { - var contextMenu = new ContextMenu (e.MouseEvent.X + 1, e.MouseEvent.Y + 1, - new MenuBarItem (new MenuItem [] { + // 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; @@ -175,11 +190,11 @@ namespace UICatalog.Scenarios { }; } - public void Save() + public void Save () { Save (tabView.SelectedTab); } - public void Save (TabView.Tab tabToSave ) + public void Save (TabView.Tab tabToSave) { var tab = tabToSave as OpenedFile; diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index a177d251c..5177b638e 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -839,6 +839,17 @@ namespace Terminal.Gui.ViewTests { 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 ()