Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI elements for textual filtering of tests #1166

Merged
merged 3 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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
/// </summary>
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;
}
}
}
19 changes: 19 additions & 0 deletions src/TestCentric/testcentric.gui/Elements/IChanged.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The IChanged interface represents a IViewElement.
/// If the IViewElement changes, it will raise the Changed event.
/// </summary>
public interface IChanged : IViewElement
{
/// <summary>
/// Event raised when the element is changed by the user
/// </summary>
event CommandHandler Changed;
}
}
118 changes: 118 additions & 0 deletions src/TestCentric/testcentric.gui/Elements/TextBoxElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// ***********************************************************************
// 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
{
/// <summary>
/// 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 Changed event as soon as no further input is made within a short period of time.
/// </summary>
public class TextBoxElement : IChanged
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public class TextBoxElement : IChanged
public class ToolStripTextBoxElement : ToolStripElement, IChanged

ToolStripElement already implements many of the basic properties. Suggested name change is consistent with our naming of other tool strip elements.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, you've convinced me 👍 !
I will rename this class to ToolStripTextBoxElement and derive it from base class ToolStripElement.

{
private Timer _typingTimer;

public event CommandHandler Changed;

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 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();
}

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 (Changed != null)
Changed();

TextBox.Focus();
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the timeout strictly necessary? I'm fairly accustomed to controls that do nothing until I tab away or otherwise change focus.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 'timeout' feature is definitely just a gimmick and not absolutely necessary.
When I was working with the text filters, I found it helpful that the results were displayed quickly without additional key stroke. And that I can continue typing immediately if I wasn't satisfied with the result. That's why I had the idea of introducing this timer. Of course, this is not a brand new idea, but some other programs use this in their search as well. So from my point of view it's beneficial for the user and I prefer to keept this feature. But of course we can remove it, if users are annoying by this feature...

Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,12 @@ private void WireUpEvents()
_model.TestCentricTestFilter.OutcomeFilter = filter;
};

_view.TextFilter.Changed += () =>
{
var text = _view.TextFilter.Text;
_model.TestCentricTestFilter.TextFilter = text;
};

// Node selected in tree
//_treeView.SelectedNodesChanged += (nodes) =>
//{
Expand Down
2 changes: 2 additions & 0 deletions src/TestCentric/testcentric.gui/Views/ITestTreeView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public interface ITestTreeView : IView
// Test Filter related properties / methods
IMultiSelection OutcomeFilter { get; }

IChanged TextFilter { get; }

void SetTestFilterVisibility(bool visible);

// Tree-related Methods
Expand Down
23 changes: 23 additions & 0 deletions src/TestCentric/testcentric.gui/Views/TestTreeView.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/TestCentric/testcentric.gui/Views/TestTreeView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -127,6 +128,8 @@ public bool CheckBoxes

public IMultiSelection OutcomeFilter { get; private set; }

public IChanged TextFilter { get; private set; }

public TreeNode ContextNode { get; private set; }
public ContextMenuStrip TreeContextMenu => TreeView.ContextMenuStrip;

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.Changed += Raise.Event<CommandHandler>();

// 3. Assert
_model.TestCentricTestFilter.Received().TextFilter = "TestA";
}

// TODO: Version 1 Test - Make it work if needed.
//[Test]
//public void WhenContextNodeIsNotNull_RunCommandExecutesThatTest()
Expand Down