From c84d77571dab3ba0ffc66ab12f836367aa40d070 Mon Sep 17 00:00:00 2001 From: rowo360 <59574371+rowo360@users.noreply.github.com> Date: Thu, 19 Dec 2024 07:25:46 +0100 Subject: [PATCH 1/3] Add UI elements for textual filtering of tests --- .../Controls/StretchToolStripTextBox.cs | 35 +++++ .../Elements/TextBoxElement.cs | 135 ++++++++++++++++++ .../Presenters/TreeViewPresenter.cs | 6 + .../testcentric.gui/Views/ITestTreeView.cs | 2 + .../Views/TestTreeView.Designer.cs | 23 +++ .../testcentric.gui/Views/TestTreeView.cs | 4 + .../TestTree/TreeViewPresenterTests.cs | 13 ++ 7 files changed, 218 insertions(+) create mode 100644 src/TestCentric/testcentric.gui/Controls/StretchToolStripTextBox.cs create mode 100644 src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs diff --git a/src/TestCentric/testcentric.gui/Controls/StretchToolStripTextBox.cs b/src/TestCentric/testcentric.gui/Controls/StretchToolStripTextBox.cs new file mode 100644 index 00000000..1cd1522d --- /dev/null +++ b/src/TestCentric/testcentric.gui/Controls/StretchToolStripTextBox.cs @@ -0,0 +1,35 @@ +// *********************************************************************** +// Copyright (c) Charlie Poole and TestCentric contributors. +// Licensed under the MIT License. See LICENSE file in root directory. +// *********************************************************************** + +using System.Drawing; +using System.Windows.Forms; +using System; + +namespace TestCentric.Gui.Controls +{ + /// + /// This class is required to stretch a ToolStripTextBox control within a ToolStrip to fill the available space and to resize when the control resizes. + /// The implementation is from the Microsoft Windows Forms documentation, but simplified to the current use case. + /// "How to: Stretch a ToolStripTextBox to Fill the Remaining Width of a ToolStrip" + /// https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/stretch-a-toolstriptextbox-to-fill-the-remaining-width-of-a-toolstrip-wf?view=netframeworkdesktop-4.8 + /// + internal class StretchToolStripTextBox : ToolStripTextBox + { + public override Size GetPreferredSize(Size constrainingSize) + { + // Get width of the owning ToolStrip + int textBoxMargin = 2; + Int32 width = Owner.DisplayRectangle.Width - textBoxMargin; + + // If the available width is less than the default width, use the default width + if (width < DefaultSize.Width) width = DefaultSize.Width; + + // Retrieve the preferred size from the base class, but change the width to the calculated width. + Size size = base.GetPreferredSize(constrainingSize); + size.Width = width; + return size; + } + } +} diff --git a/src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs b/src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs new file mode 100644 index 00000000..61973e7b --- /dev/null +++ b/src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs @@ -0,0 +1,135 @@ +// *********************************************************************** +// Copyright (c) Charlie Poole and TestCentric contributors. +// Licensed under the MIT License. See LICENSE file in root directory. +// *********************************************************************** + + +using System; +using System.Windows.Forms; + +namespace TestCentric.Gui.Elements +{ + /// + /// This class implements the ISelection interface for a TextBox control. It provides this additional functionality: + /// - show a PlaceHoder text if there's no text input + /// - Invoke the SelectionChanged event as soon as no further input is made within a short period of time. + /// + public class TextBoxElement : ISelection + { + private Timer _typingTimer; + + public event CommandHandler SelectionChanged; + + public TextBoxElement(Control textBox, string placeHolderText) + { + TextBox = textBox; + PlaceHolderText = placeHolderText; + TextBox.TextChanged += OnTextChanged; + + TextBox.LostFocus += OnTextBoxLostFocus; + TextBox.GotFocus += OnTextBoxGotFocus; + + // Call LostFocus to set initial text and color + OnTextBoxLostFocus(null, EventArgs.Empty); + } + + public string SelectedItem + { + get => TextBox.Text; + set => TextBox.Text = value; + } + + public int SelectedIndex + { + get => 0; + set => throw new NotImplementedException(); + } + + public bool Enabled + { + get => TextBox.Enabled; + set => TextBox.Enabled = value; + } + + public bool Visible + { + get => TextBox.Visible; + set => TextBox.Visible = value; + } + + public string Text + { + get => TextBox.Text; + set => TextBox.Text = value; + } + + private string PlaceHolderText { get; set; } + + private Control TextBox { get; } + + private bool IsPlaceHolderTextShown { get; set; } + + public void InvokeIfRequired(MethodInvoker _delegate) + { + throw new NotImplementedException(); + } + + public void Refresh() + { + throw new NotImplementedException(); + } + + private void OnTextBoxGotFocus(object sender, EventArgs e) + { + // If the PlaceHolderText is shown, replace it with an empty text + if (IsPlaceHolderTextShown) + { + TextBox.Text = ""; + TextBox.ForeColor = System.Drawing.Color.Black; + IsPlaceHolderTextShown = false; + } + } + + private void OnTextBoxLostFocus(object sender, EventArgs e) + { + // If there's no text input, show the PlaceHolderText instead + string searchText = TextBox.Text; + if (string.IsNullOrEmpty(searchText) && !string.IsNullOrEmpty(PlaceHolderText)) + { + IsPlaceHolderTextShown = true; + TextBox.Text = PlaceHolderText; + TextBox.ForeColor = System.Drawing.Color.LightGray; + } + } + + private void OnTextChanged(object sender, EventArgs e) + { + if (IsPlaceHolderTextShown) + return; + + if (_typingTimer == null) + { + _typingTimer = new Timer(); + _typingTimer.Interval = 600; + _typingTimer.Tick += TypingTimerTimeout; + } + + _typingTimer.Stop(); + _typingTimer.Start(); + } + + private void TypingTimerTimeout(object sender, EventArgs e) + { + var timer = sender as Timer; + if (timer == null) + return; + + // The timer must be stopped! + timer.Stop(); + if (SelectionChanged != null) + SelectionChanged(); + + TextBox.Focus(); + } + } +} diff --git a/src/TestCentric/testcentric.gui/Presenters/TreeViewPresenter.cs b/src/TestCentric/testcentric.gui/Presenters/TreeViewPresenter.cs index 722cb285..204332f9 100644 --- a/src/TestCentric/testcentric.gui/Presenters/TreeViewPresenter.cs +++ b/src/TestCentric/testcentric.gui/Presenters/TreeViewPresenter.cs @@ -257,6 +257,12 @@ private void WireUpEvents() _model.TestCentricTestFilter.OutcomeFilter = filter; }; + _view.TextFilter.SelectionChanged += () => + { + var text = _view.TextFilter.Text; + _model.TestCentricTestFilter.TextFilter = text; + }; + // Node selected in tree //_treeView.SelectedNodesChanged += (nodes) => //{ diff --git a/src/TestCentric/testcentric.gui/Views/ITestTreeView.cs b/src/TestCentric/testcentric.gui/Views/ITestTreeView.cs index 1bd1d1a1..053afb3b 100644 --- a/src/TestCentric/testcentric.gui/Views/ITestTreeView.cs +++ b/src/TestCentric/testcentric.gui/Views/ITestTreeView.cs @@ -53,6 +53,8 @@ public interface ITestTreeView : IView // Test Filter related properties / methods IMultiSelection OutcomeFilter { get; } + ISelection TextFilter { get; } + void SetTestFilterVisibility(bool visible); // Tree-related Methods diff --git a/src/TestCentric/testcentric.gui/Views/TestTreeView.Designer.cs b/src/TestCentric/testcentric.gui/Views/TestTreeView.Designer.cs index ad6fd2c2..856f4640 100644 --- a/src/TestCentric/testcentric.gui/Views/TestTreeView.Designer.cs +++ b/src/TestCentric/testcentric.gui/Views/TestTreeView.Designer.cs @@ -1,5 +1,7 @@ namespace TestCentric.Gui.Views { + using TestCentric.Gui.Controls; + partial class TestTreeView { /// @@ -37,6 +39,8 @@ private void InitializeComponent() this.filterOutcomeFailedButton = new System.Windows.Forms.ToolStripButton(); this.filterOutcomeWarningButton = new System.Windows.Forms.ToolStripButton(); this.filterOutcomeNotRunButton = new System.Windows.Forms.ToolStripButton(); + this.filterTextToolStrip = new System.Windows.Forms.ToolStrip(); + this.filterTextBox = new StretchToolStripTextBox(); this.testTreeContextMenu = new System.Windows.Forms.ContextMenuStrip(this.components); this.runMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.debugMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -128,6 +132,22 @@ private void InitializeComponent() this.filterToolStrip.Size = new System.Drawing.Size(744, 24); this.filterToolStrip.TabIndex = 0; // + // filterTextBox + // + this.filterTextBox.Name = "filterTextBox"; + // + // filterTextToolStrip + // + this.filterTextToolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.filterTextBox, + }); + this.filterTextToolStrip.Location = new System.Drawing.Point(0, 0); + this.filterTextToolStrip.Name = "filterTextToolStrip"; + this.filterTextToolStrip.Size = new System.Drawing.Size(744, 24); + this.filterTextToolStrip.Stretch = true; + this.filterTextToolStrip.Visible = false; + this.filterTextToolStrip.TabIndex = 1; + // // testTreeContextMenu // this.testTreeContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { @@ -233,6 +253,7 @@ private void InitializeComponent() this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.treeView); + this.Controls.Add(this.filterTextToolStrip); this.Controls.Add(this.filterToolStrip); this.Name = "TestTreeView"; this.Size = new System.Drawing.Size(191, 246); @@ -251,6 +272,8 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripButton filterOutcomeFailedButton; private System.Windows.Forms.ToolStripButton filterOutcomeWarningButton; private System.Windows.Forms.ToolStripButton filterOutcomeNotRunButton; + private System.Windows.Forms.ToolStrip filterTextToolStrip; + private System.Windows.Forms.ToolStripTextBox filterTextBox; private System.Windows.Forms.ToolStripMenuItem runMenuItem; private System.Windows.Forms.ToolStripMenuItem expandAllMenuItem; private System.Windows.Forms.ToolStripMenuItem collapseAllMenuItem; diff --git a/src/TestCentric/testcentric.gui/Views/TestTreeView.cs b/src/TestCentric/testcentric.gui/Views/TestTreeView.cs index c0012c5b..e301957b 100644 --- a/src/TestCentric/testcentric.gui/Views/TestTreeView.cs +++ b/src/TestCentric/testcentric.gui/Views/TestTreeView.cs @@ -49,6 +49,7 @@ public TestTreeView() TestPropertiesCommand = new CommandMenuElement(testPropertiesMenuItem); ViewAsXmlCommand = new CommandMenuElement(viewAsXmlMenuItem); OutcomeFilter = new MultiCheckedToolStripButtonGroup(new[] { filterOutcomePassedButton, filterOutcomeFailedButton, filterOutcomeWarningButton, filterOutcomeNotRunButton }); + TextFilter = new TextBoxElement(filterTextBox.Control, "Filter..."); TreeView = treeView; // NOTE: We use MouseDown here rather than MouseUp because @@ -127,6 +128,8 @@ public bool CheckBoxes public IMultiSelection OutcomeFilter { get; private set; } + public ISelection TextFilter { get; private set; } + public TreeNode ContextNode { get; private set; } public ContextMenuStrip TreeContextMenu => TreeView.ContextMenuStrip; @@ -205,6 +208,7 @@ public void InvokeIfRequired(MethodInvoker _delegate) public void SetTestFilterVisibility(bool isVisible) { filterToolStrip.Visible = isVisible; + filterTextToolStrip.Visible = isVisible; } public void LoadAlternateImages(string imageSet) diff --git a/src/TestCentric/tests/Presenters/TestTree/TreeViewPresenterTests.cs b/src/TestCentric/tests/Presenters/TestTree/TreeViewPresenterTests.cs index 93f2d24e..2de8ef16 100644 --- a/src/TestCentric/tests/Presenters/TestTree/TreeViewPresenterTests.cs +++ b/src/TestCentric/tests/Presenters/TestTree/TreeViewPresenterTests.cs @@ -249,6 +249,19 @@ public void OutcomeFilterChanged_ApplyFilter() _model.TestCentricTestFilter.Received().OutcomeFilter = selectedItems; } + [Test] + public void TextFilterChanged_ApplyFilter() + { + // 1. Arrange + _view.TextFilter.Text.Returns("TestA"); + + // 2. Act + _view.TextFilter.SelectionChanged += Raise.Event(); + + // 3. Assert + _model.TestCentricTestFilter.Received().TextFilter = "TestA"; + } + // TODO: Version 1 Test - Make it work if needed. //[Test] //public void WhenContextNodeIsNotNull_RunCommandExecutesThatTest() From ef41947fcc608a4e9666bcfa2ea5d71df128109f Mon Sep 17 00:00:00 2001 From: rowo360 <59574371+rowo360@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:02:06 +0100 Subject: [PATCH 2/3] Add interface IChanged; Use interface IChanged instead of ISelection in class TextBoxElement --- .../testcentric.gui/Elements/IChanged.cs | 19 ++++++++++++ .../Elements/TextBoxElement.cs | 29 ++++--------------- .../Presenters/TreeViewPresenter.cs | 2 +- .../testcentric.gui/Views/ITestTreeView.cs | 2 +- .../testcentric.gui/Views/TestTreeView.cs | 2 +- .../TestTree/TreeViewPresenterTests.cs | 2 +- 6 files changed, 29 insertions(+), 27 deletions(-) create mode 100644 src/TestCentric/testcentric.gui/Elements/IChanged.cs diff --git a/src/TestCentric/testcentric.gui/Elements/IChanged.cs b/src/TestCentric/testcentric.gui/Elements/IChanged.cs new file mode 100644 index 00000000..fb8911e3 --- /dev/null +++ b/src/TestCentric/testcentric.gui/Elements/IChanged.cs @@ -0,0 +1,19 @@ +// *********************************************************************** +// Copyright (c) Charlie Poole and TestCentric contributors. +// Licensed under the MIT License. See LICENSE file in root directory. +// *********************************************************************** + +namespace TestCentric.Gui.Elements +{ + /// + /// The IChanged interface represents a IViewElement. + /// If the IViewElement changes, it will raise the Changed event. + /// + public interface IChanged : IViewElement + { + /// + /// Event raised when the element is changed by the user + /// + event CommandHandler Changed; + } +} diff --git a/src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs b/src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs index 61973e7b..348e1289 100644 --- a/src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs +++ b/src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs @@ -10,15 +10,15 @@ namespace TestCentric.Gui.Elements { /// - /// This class implements the ISelection interface for a TextBox control. It provides this additional functionality: + /// This class implements the IChanged interface for a TextBox control. It provides this additional functionality: /// - show a PlaceHoder text if there's no text input - /// - Invoke the SelectionChanged event as soon as no further input is made within a short period of time. + /// - Invoke the Changed event as soon as no further input is made within a short period of time. /// - public class TextBoxElement : ISelection + public class TextBoxElement : IChanged { private Timer _typingTimer; - public event CommandHandler SelectionChanged; + public event CommandHandler Changed; public TextBoxElement(Control textBox, string placeHolderText) { @@ -33,18 +33,6 @@ public TextBoxElement(Control textBox, string placeHolderText) OnTextBoxLostFocus(null, EventArgs.Empty); } - public string SelectedItem - { - get => TextBox.Text; - set => TextBox.Text = value; - } - - public int SelectedIndex - { - get => 0; - set => throw new NotImplementedException(); - } - public bool Enabled { get => TextBox.Enabled; @@ -74,11 +62,6 @@ public void InvokeIfRequired(MethodInvoker _delegate) throw new NotImplementedException(); } - public void Refresh() - { - throw new NotImplementedException(); - } - private void OnTextBoxGotFocus(object sender, EventArgs e) { // If the PlaceHolderText is shown, replace it with an empty text @@ -126,8 +109,8 @@ private void TypingTimerTimeout(object sender, EventArgs e) // The timer must be stopped! timer.Stop(); - if (SelectionChanged != null) - SelectionChanged(); + if (Changed != null) + Changed(); TextBox.Focus(); } diff --git a/src/TestCentric/testcentric.gui/Presenters/TreeViewPresenter.cs b/src/TestCentric/testcentric.gui/Presenters/TreeViewPresenter.cs index 204332f9..bb08aecc 100644 --- a/src/TestCentric/testcentric.gui/Presenters/TreeViewPresenter.cs +++ b/src/TestCentric/testcentric.gui/Presenters/TreeViewPresenter.cs @@ -257,7 +257,7 @@ private void WireUpEvents() _model.TestCentricTestFilter.OutcomeFilter = filter; }; - _view.TextFilter.SelectionChanged += () => + _view.TextFilter.Changed += () => { var text = _view.TextFilter.Text; _model.TestCentricTestFilter.TextFilter = text; diff --git a/src/TestCentric/testcentric.gui/Views/ITestTreeView.cs b/src/TestCentric/testcentric.gui/Views/ITestTreeView.cs index 053afb3b..806608e5 100644 --- a/src/TestCentric/testcentric.gui/Views/ITestTreeView.cs +++ b/src/TestCentric/testcentric.gui/Views/ITestTreeView.cs @@ -53,7 +53,7 @@ public interface ITestTreeView : IView // Test Filter related properties / methods IMultiSelection OutcomeFilter { get; } - ISelection TextFilter { get; } + IChanged TextFilter { get; } void SetTestFilterVisibility(bool visible); diff --git a/src/TestCentric/testcentric.gui/Views/TestTreeView.cs b/src/TestCentric/testcentric.gui/Views/TestTreeView.cs index e301957b..b61bdd57 100644 --- a/src/TestCentric/testcentric.gui/Views/TestTreeView.cs +++ b/src/TestCentric/testcentric.gui/Views/TestTreeView.cs @@ -128,7 +128,7 @@ public bool CheckBoxes public IMultiSelection OutcomeFilter { get; private set; } - public ISelection TextFilter { get; private set; } + public IChanged TextFilter { get; private set; } public TreeNode ContextNode { get; private set; } public ContextMenuStrip TreeContextMenu => TreeView.ContextMenuStrip; diff --git a/src/TestCentric/tests/Presenters/TestTree/TreeViewPresenterTests.cs b/src/TestCentric/tests/Presenters/TestTree/TreeViewPresenterTests.cs index 2de8ef16..a873eb96 100644 --- a/src/TestCentric/tests/Presenters/TestTree/TreeViewPresenterTests.cs +++ b/src/TestCentric/tests/Presenters/TestTree/TreeViewPresenterTests.cs @@ -256,7 +256,7 @@ public void TextFilterChanged_ApplyFilter() _view.TextFilter.Text.Returns("TestA"); // 2. Act - _view.TextFilter.SelectionChanged += Raise.Event(); + _view.TextFilter.Changed += Raise.Event(); // 3. Assert _model.TestCentricTestFilter.Received().TextFilter = "TestA"; From ea469cfa3b0d2db7889eeb2ed60e3dd8ac9e95ac Mon Sep 17 00:00:00 2001 From: rowo360 <59574371+rowo360@users.noreply.github.com> Date: Sat, 11 Jan 2025 16:44:11 +0100 Subject: [PATCH 3/3] Rename class TextBoxElement to ToolStripTextBoxElement and derive from base class ToolStripElement --- ...xElement.cs => ToolStripTextBoxElement.cs} | 32 +++---------------- .../testcentric.gui/Views/TestTreeView.cs | 2 +- 2 files changed, 6 insertions(+), 28 deletions(-) rename src/TestCentric/testcentric.gui/Elements/{TextBoxElement.cs => ToolStripTextBoxElement.cs} (78%) diff --git a/src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs b/src/TestCentric/testcentric.gui/Elements/ToolStripTextBoxElement.cs similarity index 78% rename from src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs rename to src/TestCentric/testcentric.gui/Elements/ToolStripTextBoxElement.cs index 348e1289..787c2d27 100644 --- a/src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs +++ b/src/TestCentric/testcentric.gui/Elements/ToolStripTextBoxElement.cs @@ -10,17 +10,18 @@ namespace TestCentric.Gui.Elements { /// - /// This class implements the IChanged interface for a TextBox control. It provides this additional functionality: + /// This class implements the IChanged interface for a ToolStripTextBox control. It provides this additional functionality: /// - show a PlaceHoder text if there's no text input /// - Invoke the Changed event as soon as no further input is made within a short period of time. /// - public class TextBoxElement : IChanged + public class ToolStripTextBoxElement : ToolStripElement, IChanged { private Timer _typingTimer; public event CommandHandler Changed; - public TextBoxElement(Control textBox, string placeHolderText) + public ToolStripTextBoxElement(ToolStripTextBox textBox, string placeHolderText) + : base(textBox) { TextBox = textBox; PlaceHolderText = placeHolderText; @@ -33,35 +34,12 @@ public TextBoxElement(Control textBox, string placeHolderText) OnTextBoxLostFocus(null, EventArgs.Empty); } - public bool Enabled - { - get => TextBox.Enabled; - set => TextBox.Enabled = value; - } - - public bool Visible - { - get => TextBox.Visible; - set => TextBox.Visible = value; - } - - public string Text - { - get => TextBox.Text; - set => TextBox.Text = value; - } - private string PlaceHolderText { get; set; } - private Control TextBox { get; } + private ToolStripTextBox TextBox { get; } private bool IsPlaceHolderTextShown { get; set; } - public void InvokeIfRequired(MethodInvoker _delegate) - { - throw new NotImplementedException(); - } - private void OnTextBoxGotFocus(object sender, EventArgs e) { // If the PlaceHolderText is shown, replace it with an empty text diff --git a/src/TestCentric/testcentric.gui/Views/TestTreeView.cs b/src/TestCentric/testcentric.gui/Views/TestTreeView.cs index b61bdd57..1c4cc172 100644 --- a/src/TestCentric/testcentric.gui/Views/TestTreeView.cs +++ b/src/TestCentric/testcentric.gui/Views/TestTreeView.cs @@ -49,7 +49,7 @@ public TestTreeView() TestPropertiesCommand = new CommandMenuElement(testPropertiesMenuItem); ViewAsXmlCommand = new CommandMenuElement(viewAsXmlMenuItem); OutcomeFilter = new MultiCheckedToolStripButtonGroup(new[] { filterOutcomePassedButton, filterOutcomeFailedButton, filterOutcomeWarningButton, filterOutcomeNotRunButton }); - TextFilter = new TextBoxElement(filterTextBox.Control, "Filter..."); + TextFilter = new ToolStripTextBoxElement(filterTextBox, "Filter..."); TreeView = treeView; // NOTE: We use MouseDown here rather than MouseUp because